Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afed3aed53 | |||
| ead8f23379 | |||
| 4d31f9f71a | |||
| 880df2c2ac | |||
| c40302aa93 | |||
| b424376656 | |||
| 788ca3519f | |||
| 3a0f43d491 | |||
| 6ecb29eb5e | |||
| 96a838eb8e | |||
| 8795de6c7d | |||
| 5bc0e9220a | |||
| d05052b5e3 | |||
| 15f9ae928c | |||
| 6983d5724a | |||
| 36cb82d335 | |||
| 993bf970d1 | |||
| 0db5333242 | |||
| 317617c3ce | |||
| a5b30367cb | |||
| e6912e2541 | |||
| fdb173e558 | |||
| efacc0afcf | |||
| 8c9c37cd8d | |||
| 9469c76a23 | |||
| 3dc66e8fa0 | |||
| ae1dba3da9 | |||
| e6dfcc8cf7 | |||
| b0d0579ce7 | |||
| cb85501f7c | |||
| 8f50650e49 | |||
| f56f3f3dc2 | |||
| 5a680f5f0e | |||
| b78e7b159b | |||
| dbc9bffec4 | |||
| 71347c0ace | |||
| 99ff2474e8 | |||
| ccfc694586 | |||
| 6cc67fd174 | |||
| d8a597e615 | |||
| 9fe7ccd1a3 | |||
| 592e9e4dee | |||
| 2892395c8b | |||
| ae8294841c | |||
| 578aaf1ab6 | |||
| cc6c57655e | |||
| b11e5db2e7 | |||
| 58990ced91 | |||
| 31d1f0bc0d | |||
| 4dd5ce2275 | |||
| 97fec278e6 | |||
| 8765187e60 | |||
| 6614303b4b | |||
| 0885eef073 | |||
| 0dc8813925 | |||
| 0464e64c60 | |||
| 428c5d9210 | |||
| 55fa3f9da0 | |||
| 0e5e0cf0af | |||
| 67e92c92f9 | |||
| 636c50d4ce | |||
| dc1ded81b8 | |||
| 85706a5819 | |||
| 260d853f30 | |||
| fd7fda2f77 | |||
| 72f295df61 | |||
| 2c73035c63 | |||
| 9f90a14e16 | |||
| 7a5ff1a420 | |||
| d35f6b7da2 | |||
| 1236746ce9 | |||
| d5f0da4b65 | |||
| adaa7a72dd | |||
| fe8713b966 | |||
| 4d10eba0e7 | |||
| fd59c53372 | |||
| 2b61f24e45 | |||
| 9025deb6fc | |||
| b913ae75a6 | |||
| a44f366ba1 | |||
| aa697cb677 |
14
.eslintrc
14
.eslintrc
@@ -8,12 +8,15 @@
|
|||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"requireConfigFile": false,
|
"requireConfigFile": false,
|
||||||
"babelOptions": {
|
"babelOptions": {
|
||||||
"presets": ["@babel/preset-react"]
|
"presets": [
|
||||||
|
"@babel/preset-react"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"react-app",
|
"react-app",
|
||||||
"airbnb",
|
"airbnb",
|
||||||
|
"prettier",
|
||||||
"plugin:prettier/recommended"
|
"plugin:prettier/recommended"
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
@@ -58,7 +61,12 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"import/no-extraneous-dependencies": ["warn", {"devDependencies": true}],
|
"import/no-extraneous-dependencies": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"devDependencies": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"no-use-before-define": "off",
|
"no-use-before-define": "off",
|
||||||
|
|||||||
15
.prettierrc
Normal file
15
.prettierrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"printWidth": 100,
|
||||||
|
"proseWrap": "always",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"useTabs": false,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
16
CREDITS
16
CREDITS
@@ -1,16 +0,0 @@
|
|||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
// @ Image/Vector/Icon Credits
|
|
||||||
// -----------------------------------------------------------------------------------------------------
|
|
||||||
Avatars - https://uifaces.co/
|
|
||||||
Flag icons - http://www.famfamfam.com/lab/icons/flags/
|
|
||||||
Frame vector created by Freepik - https://www.freepik.com/free-photos-vectors/frame
|
|
||||||
A Walk Amongst Friends - Photo by Kristin Ellis on Unsplash - https://unsplash.com/photos/CbZOGbazDWQ
|
|
||||||
Sunrise at Moraine Lake - Photo by Marlon Martinez on Unsplash - https://unsplash.com/photos/woNYcfrnp9M
|
|
||||||
Braies Lake - Photo by Luca Nicoletti on Unsplash - https://unsplash.com/photos/dH-L5zPcv3E
|
|
||||||
Lago di Sorapis - Photo by eberhard grossgasteiger on Unsplash - https://unsplash.com/photos/6uDg_zb20EM
|
|
||||||
Lago di Braies - Photo by Salmen Bejaoui on Unsplash - https://unsplash.com/photos/uXTozY3CcQg
|
|
||||||
Reaching - Photo by Justin Novello on Unsplash - https://unsplash.com/photos/Y14TNvIDllM
|
|
||||||
Yosemite - Photo by Tim Mossholder on Unsplash - https://unsplash.com/photos/ZCrtRSSUpGI
|
|
||||||
Never Stop Changing - Photo by John Westrock on Unsplash - https://unsplash.com/photos/_GY56uSG70U
|
|
||||||
Fall glow - Photo by Casey Horner on Unsplash - https://unsplash.com/photos/gz19zOdgN7w
|
|
||||||
First snow - Photo by eberhard grossgasteiger on Unsplash - https://unsplash.com/photos/LRrGf6dBjA4
|
|
||||||
@@ -7,7 +7,7 @@ const aliases = (prefix = `src`) => ({
|
|||||||
'app/shared-components': `${prefix}/app/shared-components`,
|
'app/shared-components': `${prefix}/app/shared-components`,
|
||||||
'app/configs': `${prefix}/app/configs`,
|
'app/configs': `${prefix}/app/configs`,
|
||||||
'app/theme-layouts': `${prefix}/app/theme-layouts`,
|
'app/theme-layouts': `${prefix}/app/theme-layouts`,
|
||||||
'app/AppContext': `${prefix}/app/AppContext`,
|
'app/contexts/AppContext': `${prefix}/app/contexts/AppContext`,
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = aliases;
|
module.exports = aliases;
|
||||||
|
|||||||
21
database.rules.json
Normal file
21
database.rules.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
".read": false,
|
||||||
|
".write": false,
|
||||||
|
"users": {
|
||||||
|
"$userId": {
|
||||||
|
".read": "auth.uid === $userId",
|
||||||
|
".write": "auth.uid === $userId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// // readable node
|
||||||
|
// "messages": {
|
||||||
|
// ".read": true
|
||||||
|
// },
|
||||||
|
// // readable and writable node
|
||||||
|
// "messages": {
|
||||||
|
// ".read": true,
|
||||||
|
// ".write": true
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,34 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@fuse/*": ["./src/@fuse/*"],
|
"@fuse/*": [
|
||||||
"@history*": ["./src/@history"],
|
"./src/@fuse/*"
|
||||||
"@lodash": ["./src/@lodash"],
|
],
|
||||||
"@mock-api": ["./src/@mock-api"],
|
"@history*": [
|
||||||
"app/store/*": ["./src/app/store/*"],
|
"./src/@history"
|
||||||
"app/shared-components/*": ["./src/app/shared-components/*"],
|
],
|
||||||
"app/configs/*": ["./src/app/configs/*"],
|
"@lodash": [
|
||||||
"app/theme-layouts/*": ["./src/app/theme-layouts/*"],
|
"./src/@lodash"
|
||||||
"app/AppContext": ["./src/app/AppContext"]
|
],
|
||||||
}
|
"@mock-api": [
|
||||||
|
"./src/@mock-api"
|
||||||
|
],
|
||||||
|
"app/store/*": [
|
||||||
|
"./src/app/store/*"
|
||||||
|
],
|
||||||
|
"app/shared-components/*": [
|
||||||
|
"./src/app/shared-components/*"
|
||||||
|
],
|
||||||
|
"app/configs/*": [
|
||||||
|
"./src/app/configs/*"
|
||||||
|
],
|
||||||
|
"app/theme-layouts/*": [
|
||||||
|
"./src/app/theme-layouts/*"
|
||||||
|
],
|
||||||
|
"app/contexts/AppContext": [
|
||||||
|
"./src/app/contexts/AppContext"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1440
package-lock.json
generated
1440
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@
|
|||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
"draft-js": "0.11.7",
|
"draft-js": "0.11.7",
|
||||||
"draftjs-to-html": "0.9.1",
|
"draftjs-to-html": "0.9.1",
|
||||||
|
"firebase": "^9.22.1",
|
||||||
"framer-motion": "10.10.0",
|
"framer-motion": "10.10.0",
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
"i18next": "22.4.14",
|
"i18next": "22.4.14",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* HEY HO
|
* HEY HO
|
||||||
*/
|
*/
|
||||||
@@ -14,15 +13,15 @@
|
|||||||
*,
|
*,
|
||||||
::before,
|
::before,
|
||||||
::after {
|
::after {
|
||||||
box-sizing: border-box; /* 1 */
|
box-sizing: border-box; /* 1 */
|
||||||
border-width: 0; /* 2 */
|
border-width: 0; /* 2 */
|
||||||
border-style: solid; /* 2 */
|
border-style: solid; /* 2 */
|
||||||
border-color: #EEEEEE; /* 2 */
|
border-color: #eeeeee; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
::before,
|
::before,
|
||||||
::after {
|
::after {
|
||||||
--tw-content: '';
|
--tw-content: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -33,10 +32,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
html {
|
html {
|
||||||
line-height: 1.5; /* 1 */
|
line-height: 1.5; /* 1 */
|
||||||
-webkit-text-size-adjust: 100%; /* 2 */ /* 3 */
|
-webkit-text-size-adjust: 100%; /* 2 */ /* 3 */
|
||||||
tab-size: 4; /* 3 */
|
tab-size: 4; /* 3 */
|
||||||
font-family: Inter var, Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */
|
font-family: Inter var, Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||||
|
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||||
|
'Segoe UI Symbol', 'Noto Color Emoji'; /* 4 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -45,8 +46,8 @@ html {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0; /* 1 */
|
margin: 0; /* 1 */
|
||||||
line-height: inherit; /* 2 */
|
line-height: inherit; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -56,9 +57,9 @@ body {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
height: 0; /* 1 */
|
height: 0; /* 1 */
|
||||||
color: inherit; /* 2 */
|
color: inherit; /* 2 */
|
||||||
border-top-width: 1px; /* 3 */
|
border-top-width: 1px; /* 3 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -66,7 +67,7 @@ Add the correct text decoration in Chrome, Edge, and Safari.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
abbr:where([title]) {
|
abbr:where([title]) {
|
||||||
text-decoration: underline dotted;
|
text-decoration: underline dotted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -79,8 +80,8 @@ h3,
|
|||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6 {
|
h6 {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -88,8 +89,8 @@ Reset links to optimize for opt-in styling instead of opt-out.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -98,7 +99,7 @@ Add the correct font weight in Edge and Safari.
|
|||||||
|
|
||||||
b,
|
b,
|
||||||
strong {
|
strong {
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -110,8 +111,9 @@ code,
|
|||||||
kbd,
|
kbd,
|
||||||
samp,
|
samp,
|
||||||
pre {
|
pre {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||||
font-size: 1em; /* 2 */
|
'Courier New', monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -119,7 +121,7 @@ Add the correct font size in all browsers.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
small {
|
small {
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -128,18 +130,18 @@ Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
|||||||
|
|
||||||
sub,
|
sub,
|
||||||
sup {
|
sup {
|
||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub {
|
sub {
|
||||||
bottom: -0.25em;
|
bottom: -0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
sup {
|
sup {
|
||||||
top: -0.5em;
|
top: -0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -149,9 +151,9 @@ sup {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
table {
|
table {
|
||||||
text-indent: 0; /* 1 */
|
text-indent: 0; /* 1 */
|
||||||
border-color: inherit; /* 2 */
|
border-color: inherit; /* 2 */
|
||||||
border-collapse: collapse; /* 3 */
|
border-collapse: collapse; /* 3 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -165,12 +167,12 @@ input,
|
|||||||
optgroup,
|
optgroup,
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
font-family: inherit; /* 1 */
|
font-family: inherit; /* 1 */
|
||||||
font-size: 100%; /* 1 */
|
font-size: 100%; /* 1 */
|
||||||
line-height: inherit; /* 1 */
|
line-height: inherit; /* 1 */
|
||||||
color: inherit; /* 1 */
|
color: inherit; /* 1 */
|
||||||
margin: 0; /* 2 */
|
margin: 0; /* 2 */
|
||||||
padding: 0; /* 3 */
|
padding: 0; /* 3 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -179,7 +181,7 @@ Remove the inheritance of text transform in Edge and Firefox.
|
|||||||
|
|
||||||
button,
|
button,
|
||||||
select {
|
select {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -191,9 +193,9 @@ button,
|
|||||||
[type='button'],
|
[type='button'],
|
||||||
[type='reset'],
|
[type='reset'],
|
||||||
[type='submit'] {
|
[type='submit'] {
|
||||||
-webkit-appearance: button; /* 1 */
|
-webkit-appearance: button; /* 1 */
|
||||||
background-color: transparent; /* 2 */
|
background-color: transparent; /* 2 */
|
||||||
background-image: none; /* 2 */
|
background-image: none; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -201,7 +203,7 @@ Use the modern Firefox focus style for all focusable elements.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
:-moz-focusring {
|
:-moz-focusring {
|
||||||
outline: auto;
|
outline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -209,7 +211,7 @@ Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
:-moz-ui-invalid {
|
:-moz-ui-invalid {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -217,7 +219,7 @@ Add the correct vertical alignment in Chrome and Firefox.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
progress {
|
progress {
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -226,7 +228,7 @@ Correct the cursor style of increment and decrement buttons in Safari.
|
|||||||
|
|
||||||
::-webkit-inner-spin-button,
|
::-webkit-inner-spin-button,
|
||||||
::-webkit-outer-spin-button {
|
::-webkit-outer-spin-button {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -235,8 +237,8 @@ Correct the cursor style of increment and decrement buttons in Safari.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
[type='search'] {
|
[type='search'] {
|
||||||
-webkit-appearance: textfield; /* 1 */
|
-webkit-appearance: textfield; /* 1 */
|
||||||
outline-offset: -2px; /* 2 */
|
outline-offset: -2px; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -244,7 +246,7 @@ Remove the inner padding in Chrome and Safari on macOS.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
::-webkit-search-decoration {
|
::-webkit-search-decoration {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -253,8 +255,8 @@ Remove the inner padding in Chrome and Safari on macOS.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
::-webkit-file-upload-button {
|
::-webkit-file-upload-button {
|
||||||
-webkit-appearance: button; /* 1 */
|
-webkit-appearance: button; /* 1 */
|
||||||
font: inherit; /* 2 */
|
font: inherit; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -262,7 +264,7 @@ Add the correct display in Chrome and Safari.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
summary {
|
summary {
|
||||||
display: list-item;
|
display: list-item;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -282,24 +284,24 @@ hr,
|
|||||||
figure,
|
figure,
|
||||||
p,
|
p,
|
||||||
pre {
|
pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
legend {
|
legend {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ol,
|
ol,
|
||||||
ul,
|
ul,
|
||||||
menu {
|
menu {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -307,7 +309,7 @@ Prevent resizing textareas horizontally by default.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -317,8 +319,8 @@ textarea {
|
|||||||
|
|
||||||
input::placeholder,
|
input::placeholder,
|
||||||
textarea::placeholder {
|
textarea::placeholder {
|
||||||
opacity: 1; /* 1 */
|
opacity: 1; /* 1 */
|
||||||
color: #BDBDBD; /* 2 */
|
color: #bdbdbd; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -326,8 +328,8 @@ Set the default cursor for buttons.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
button,
|
button,
|
||||||
[role="button"] {
|
[role='button'] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -335,7 +337,7 @@ Make sure disabled buttons don't get the pointer cursor.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
:disabled {
|
:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -352,8 +354,8 @@ audio,
|
|||||||
iframe,
|
iframe,
|
||||||
embed,
|
embed,
|
||||||
object {
|
object {
|
||||||
display: block; /* 1 */
|
display: block; /* 1 */
|
||||||
vertical-align: middle; /* 2 */
|
vertical-align: middle; /* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -362,8 +364,8 @@ Constrain images and videos to the parent width and preserve their intrinsic asp
|
|||||||
|
|
||||||
img,
|
img,
|
||||||
video {
|
video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -371,50 +373,52 @@ Ensure the default browser behavior of the `hidden` attribute.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
[hidden] {
|
[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
*, ::before, ::after {
|
*,
|
||||||
--tw-translate-x: 0;
|
::before,
|
||||||
--tw-translate-y: 0;
|
::after {
|
||||||
--tw-rotate: 0;
|
--tw-translate-x: 0;
|
||||||
--tw-skew-x: 0;
|
--tw-translate-y: 0;
|
||||||
--tw-skew-y: 0;
|
--tw-rotate: 0;
|
||||||
--tw-scale-x: 1;
|
--tw-skew-x: 0;
|
||||||
--tw-scale-y: 1;
|
--tw-skew-y: 0;
|
||||||
--tw-pan-x: '';
|
--tw-scale-x: 1;
|
||||||
--tw-pan-y: '';
|
--tw-scale-y: 1;
|
||||||
--tw-pinch-zoom: '';
|
--tw-pan-x: '';
|
||||||
--tw-scroll-snap-strictness: proximity;
|
--tw-pan-y: '';
|
||||||
--tw-ordinal: '';
|
--tw-pinch-zoom: '';
|
||||||
--tw-slashed-zero: '';
|
--tw-scroll-snap-strictness: proximity;
|
||||||
--tw-numeric-figure: '';
|
--tw-ordinal: '';
|
||||||
--tw-numeric-spacing: '';
|
--tw-slashed-zero: '';
|
||||||
--tw-numeric-fraction: '';
|
--tw-numeric-figure: '';
|
||||||
--tw-ring-inset: '';
|
--tw-numeric-spacing: '';
|
||||||
--tw-ring-offset-width: 0px;
|
--tw-numeric-fraction: '';
|
||||||
--tw-ring-offset-color: #fff;
|
--tw-ring-inset: '';
|
||||||
--tw-ring-color: rgb(33 150 243 / 0.5);
|
--tw-ring-offset-width: 0px;
|
||||||
--tw-ring-offset-shadow: 0 0 #0000;
|
--tw-ring-offset-color: #fff;
|
||||||
--tw-ring-shadow: 0 0 #0000;
|
--tw-ring-color: rgb(33 150 243 / 0.5);
|
||||||
--tw-shadow: 0 0 #0000;
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
--tw-shadow-colored: 0 0 #0000;
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
--tw-blur: '';
|
--tw-shadow: 0 0 #0000;
|
||||||
--tw-brightness: '';
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
--tw-contrast: '';
|
--tw-blur: '';
|
||||||
--tw-grayscale: '';
|
--tw-brightness: '';
|
||||||
--tw-hue-rotate: '';
|
--tw-contrast: '';
|
||||||
--tw-invert: '';
|
--tw-grayscale: '';
|
||||||
--tw-saturate: '';
|
--tw-hue-rotate: '';
|
||||||
--tw-sepia: '';
|
--tw-invert: '';
|
||||||
--tw-drop-shadow: '';
|
--tw-saturate: '';
|
||||||
--tw-backdrop-blur: '';
|
--tw-sepia: '';
|
||||||
--tw-backdrop-brightness: '';
|
--tw-drop-shadow: '';
|
||||||
--tw-backdrop-contrast: '';
|
--tw-backdrop-blur: '';
|
||||||
--tw-backdrop-grayscale: '';
|
--tw-backdrop-brightness: '';
|
||||||
--tw-backdrop-hue-rotate: '';
|
--tw-backdrop-contrast: '';
|
||||||
--tw-backdrop-invert: '';
|
--tw-backdrop-grayscale: '';
|
||||||
--tw-backdrop-opacity: '';
|
--tw-backdrop-hue-rotate: '';
|
||||||
--tw-backdrop-saturate: '';
|
--tw-backdrop-invert: '';
|
||||||
--tw-backdrop-sepia: '';
|
--tw-backdrop-opacity: '';
|
||||||
|
--tw-backdrop-saturate: '';
|
||||||
|
--tw-backdrop-sepia: '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,155 +1,129 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang='en'>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf-8'>
|
<meta charset="utf-8" />
|
||||||
<meta name='description' content='Fuse React - Material design admin template with pre-built apps and pages'>
|
<meta name="description" content="Rental Calculator" />
|
||||||
<meta name='keywords'
|
<meta name="keywords" content="Real estate" />
|
||||||
content='React,Redux,Material UI Next,Material,Material Design,Google Material Design,HTML,CSS,Firebase,Authentication,Material Redux Theme,Material Redux Template'>
|
<meta name="author" content="Withinpixels" />
|
||||||
<meta name='author' content='Withinpixels'>
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name='theme-color' content='#000000'>
|
<base href="/" />
|
||||||
<base href='/'>
|
|
||||||
|
|
||||||
<link href='%PUBLIC_URL%/assets/tailwind-base.css' rel='stylesheet'>
|
<link href="%PUBLIC_URL%/assets/tailwind-base.css" rel="stylesheet" />
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<!--<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">-->
|
||||||
|
<!-- You can choose main icon from variety of the material ui icon fonts-->
|
||||||
|
<link
|
||||||
|
href="%PUBLIC_URL%/assets/fonts/material-design-icons/MaterialIconsOutlined.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<!-- <link href="%PUBLIC_URL%/assets/fonts/material-design-icons/MaterialIcons.css" rel="stylesheet">-->
|
||||||
|
<!-- <link href="%PUBLIC_URL%/assets/fonts/material-design-icons/MaterialIconsRound.css" rel="stylesheet">-->
|
||||||
|
<!-- <link href="%PUBLIC_URL%/assets/fonts/material-design-icons/MaterialIconsSharp.css" rel="stylesheet">-->
|
||||||
|
<!-- <link href="%PUBLIC_URL%/assets/fonts/material-design-icons/MaterialIconsTwoTone.css" rel="stylesheet">-->
|
||||||
|
<link href="%PUBLIC_URL%/assets/fonts/inter/inter.css" rel="stylesheet" />
|
||||||
|
<link href="%PUBLIC_URL%/assets/fonts/meteocons/style.css" rel="stylesheet" />
|
||||||
|
|
||||||
<!--
|
<noscript id="emotion-insertion-point"></noscript>
|
||||||
manifest.json provides metadata used when your web app is added to the
|
<title>Rental Calculator</title>
|
||||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel='manifest' href='%PUBLIC_URL%/manifest.json'>
|
|
||||||
<link rel='shortcut icon' href='%PUBLIC_URL%/favicon.ico'>
|
|
||||||
|
|
||||||
<!--<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">-->
|
<!-- FUSE Splash Screen CSS -->
|
||||||
|
<style>
|
||||||
|
body #fuse-splash-screen {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #111827;
|
||||||
|
color: #f9fafb;
|
||||||
|
z-index: 999999;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transition: opacity 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
<!-- You can choose main icon from variety of the material ui icon fonts-->
|
body #fuse-splash-screen img {
|
||||||
<link href='%PUBLIC_URL%/assets/fonts/material-design-icons/MaterialIconsOutlined.css' rel='stylesheet'>
|
width: 120px;
|
||||||
<!-- <link href="%PUBLIC_URL%/assets/fonts/material-design-icons/MaterialIcons.css" rel="stylesheet">-->
|
max-width: 120px;
|
||||||
<!-- <link href="%PUBLIC_URL%/assets/fonts/material-design-icons/MaterialIconsRound.css" rel="stylesheet">-->
|
}
|
||||||
<!-- <link href="%PUBLIC_URL%/assets/fonts/material-design-icons/MaterialIconsSharp.css" rel="stylesheet">-->
|
|
||||||
<!-- <link href="%PUBLIC_URL%/assets/fonts/material-design-icons/MaterialIconsTwoTone.css" rel="stylesheet">-->
|
|
||||||
|
|
||||||
<link href='%PUBLIC_URL%/assets/fonts/inter/inter.css' rel='stylesheet'>
|
#spinner {
|
||||||
<link href='%PUBLIC_URL%/assets/fonts/meteocons/style.css' rel='stylesheet'>
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 40px;
|
||||||
|
width: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
<noscript id='emotion-insertion-point'></noscript>
|
#spinner > div {
|
||||||
<!--
|
width: 12px;
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
height: 12px;
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
background-color: #1e96f7;
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
border-radius: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
-webkit-animation: fuse-bouncedelay 1s infinite ease-in-out both;
|
||||||
|
animation: fuse-bouncedelay 1s infinite ease-in-out both;
|
||||||
|
}
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
#spinner .bounce1 {
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
-webkit-animation-delay: -0.32s;
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
animation-delay: -0.32s;
|
||||||
-->
|
}
|
||||||
<title>Fuse React - Material Design Admin Template</title>
|
|
||||||
|
|
||||||
<!-- FUSE Splash Screen CSS -->
|
#spinner .bounce2 {
|
||||||
<style>
|
-webkit-animation-delay: -0.16s;
|
||||||
body #fuse-splash-screen {
|
animation-delay: -0.16s;
|
||||||
display: flex;
|
}
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: #111827;
|
|
||||||
color: #F9FAFB;
|
|
||||||
z-index: 999999;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
transition: opacity 400ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
body #fuse-splash-screen img {
|
@-webkit-keyframes fuse-bouncedelay {
|
||||||
width: 120px;
|
0%,
|
||||||
max-width: 120px;
|
80%,
|
||||||
}
|
100% {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#spinner {
|
@keyframes fuse-bouncedelay {
|
||||||
display: flex;
|
0%,
|
||||||
align-items: center;
|
80%,
|
||||||
justify-content: space-between;
|
100% {
|
||||||
margin-top: 40px;
|
-webkit-transform: scale(0);
|
||||||
width: 56px;
|
transform: scale(0);
|
||||||
}
|
}
|
||||||
|
40% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!-- / FUSE Splash Screen CSS -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||||
|
|
||||||
#spinner > div {
|
<div id="root" class="flex">
|
||||||
width: 12px;
|
<!-- FUSE Splash Screen -->
|
||||||
height: 12px;
|
<div id="fuse-splash-screen">
|
||||||
background-color: #1E96F7;
|
<div class="logo">
|
||||||
border-radius: 100%;
|
<img width="128" src="assets/images/logo/logo.svg" alt="logo" />
|
||||||
display: inline-block;
|
|
||||||
-webkit-animation: fuse-bouncedelay 1s infinite ease-in-out both;
|
|
||||||
animation: fuse-bouncedelay 1s infinite ease-in-out both;
|
|
||||||
}
|
|
||||||
|
|
||||||
#spinner .bounce1 {
|
|
||||||
-webkit-animation-delay: -0.32s;
|
|
||||||
animation-delay: -0.32s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#spinner .bounce2 {
|
|
||||||
-webkit-animation-delay: -0.16s;
|
|
||||||
animation-delay: -0.16s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes fuse-bouncedelay {
|
|
||||||
0%, 80%, 100% {
|
|
||||||
-webkit-transform: scale(0)
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
-webkit-transform: scale(1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fuse-bouncedelay {
|
|
||||||
0%, 80%, 100% {
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
-webkit-transform: scale(1.0);
|
|
||||||
transform: scale(1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
<!-- / FUSE Splash Screen CSS -->
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<noscript>
|
|
||||||
You need to enable JavaScript to run this app.
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<div id='root' class='flex'>
|
|
||||||
<!-- FUSE Splash Screen -->
|
|
||||||
<div id='fuse-splash-screen'>
|
|
||||||
<div class='logo'>
|
|
||||||
<img width='128' src='assets/images/logo/logo.svg' alt='logo'>
|
|
||||||
</div>
|
|
||||||
<div id='spinner'>
|
|
||||||
<div class='bounce1'></div>
|
|
||||||
<div class='bounce2'></div>
|
|
||||||
<div class='bounce3'></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- / FUSE Splash Screen -->
|
<div id="spinner">
|
||||||
|
<div class="bounce1"></div>
|
||||||
|
<div class="bounce2"></div>
|
||||||
|
<div class="bounce3"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- / FUSE Splash Screen -->
|
||||||
</div>
|
</div>
|
||||||
<!--
|
</body>
|
||||||
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>
|
</html>
|
||||||
|
|||||||
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
|
Sitemap: /sitemap.xml
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import FuseUtils from '@fuse/utils';
|
import FuseUtils from '@fuse/utils';
|
||||||
import AppContext from 'app/AppContext';
|
import AppContext from 'src/app/contexts/AppContext';
|
||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { matchRoutes } from 'react-router-dom';
|
import { matchRoutes } from 'react-router-dom';
|
||||||
import withRouter from '@fuse/core/withRouter';
|
import withRouter from '@fuse/core/withRouter';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useDeepCompareEffect } from '@fuse/hooks';
|
import { useDeepCompareEffect } from '@fuse/hooks';
|
||||||
import _ from '@lodash';
|
import _ from '@lodash';
|
||||||
import AppContext from 'app/AppContext';
|
import AppContext from 'src/app/contexts/AppContext';
|
||||||
import {
|
import {
|
||||||
generateSettings,
|
generateSettings,
|
||||||
selectFuseCurrentSettings,
|
selectFuseCurrentSettings,
|
||||||
@@ -11,7 +11,6 @@ import { memo, useCallback, useContext, useMemo, useRef } from 'react';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { matchRoutes, useLocation } from 'react-router-dom';
|
import { matchRoutes, useLocation } from 'react-router-dom';
|
||||||
import GlobalStyles from '@mui/material/GlobalStyles';
|
import GlobalStyles from '@mui/material/GlobalStyles';
|
||||||
import { alpha } from '@mui/material/styles';
|
|
||||||
|
|
||||||
const inputGlobalStyles = (
|
const inputGlobalStyles = (
|
||||||
<GlobalStyles
|
<GlobalStyles
|
||||||
@@ -38,27 +37,30 @@ const inputGlobalStyles = (
|
|||||||
'table.simple thead tr th': {
|
'table.simple thead tr th': {
|
||||||
borderColor: theme.palette.divider,
|
borderColor: theme.palette.divider,
|
||||||
},
|
},
|
||||||
'a:not([role=button]):not(.MuiButtonBase-root)': {
|
// 'a:not([role=button]):not(.MuiButtonBase-root)': {
|
||||||
color: theme.palette.secondary.main,
|
// color: theme.palette.secondary.main,
|
||||||
textDecoration: 'underline',
|
// textDecoration: 'underline',
|
||||||
'&:hover': {},
|
// '&:hover': {},
|
||||||
},
|
// },
|
||||||
'a.link, a:not([role=button])[target=_blank]': {
|
// 'a.link, a:not([role=button])[target=_blank]': {
|
||||||
background: alpha(theme.palette.secondary.main, 0.2),
|
// background: alpha(theme.palette.secondary.main, 0.2),
|
||||||
color: 'inherit',
|
// color: 'inherit',
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
// borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
textDecoration: 'none',
|
// textDecoration: 'none',
|
||||||
'&:hover': {
|
// '&:hover': {
|
||||||
background: alpha(theme.palette.secondary.main, 0.3),
|
// background: alpha(theme.palette.secondary.main, 0.3),
|
||||||
textDecoration: 'none',
|
// textDecoration: 'none',
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
'[class^="border"]': {
|
// '[class*="MuiOutlinedInput-root"]': {
|
||||||
borderColor: theme.palette.divider,
|
// borderRadius: `${theme.spacing('10px')}`,
|
||||||
},
|
// },
|
||||||
'[class*="border"]': {
|
// '[class^="border"]': {
|
||||||
borderColor: theme.palette.divider,
|
// borderColor: theme.palette.divider,
|
||||||
},
|
// },
|
||||||
|
// '[class*="border"]': {
|
||||||
|
// borderColor: theme.palette.divider,
|
||||||
|
// },
|
||||||
'[class*="divide-"] > :not([hidden]) ~ :not([hidden])': {
|
'[class*="divide-"] > :not([hidden]) ~ :not([hidden])': {
|
||||||
borderColor: theme.palette.divider,
|
borderColor: theme.palette.divider,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { selectMainTheme } from 'app/store/fuse/settingsSlice';
|
|||||||
import FuseAuthorization from '@fuse/core/FuseAuthorization';
|
import FuseAuthorization from '@fuse/core/FuseAuthorization';
|
||||||
import settingsConfig from 'app/configs/settingsConfig';
|
import settingsConfig from 'app/configs/settingsConfig';
|
||||||
import withAppProviders from './withAppProviders';
|
import withAppProviders from './withAppProviders';
|
||||||
import { AuthProvider } from './auth/AuthContext';
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
|
|
||||||
// import axios from 'axios';
|
// import axios from 'axios';
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import FuseSplashScreen from '@fuse/core/FuseSplashScreen';
|
|
||||||
import { showMessage } from 'app/store/fuse/messageSlice';
|
|
||||||
import { logoutUser, setUser } from 'app/store/userSlice';
|
|
||||||
import jwtService from './services/jwtService';
|
|
||||||
|
|
||||||
const AuthContext = React.createContext();
|
|
||||||
|
|
||||||
function AuthProvider({ children }) {
|
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(undefined);
|
|
||||||
const [waitAuthCheck, setWaitAuthCheck] = useState(true);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
jwtService.on('onAutoLogin', () => {
|
|
||||||
dispatch(showMessage({ message: 'Signing in with JWT' }));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign in and retrieve user data with stored token
|
|
||||||
*/
|
|
||||||
jwtService
|
|
||||||
.signInWithToken()
|
|
||||||
.then((user) => {
|
|
||||||
success(user, 'Signed in with JWT');
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
pass(error.message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
jwtService.on('onLogin', (user) => {
|
|
||||||
success(user, 'Signed in');
|
|
||||||
});
|
|
||||||
|
|
||||||
jwtService.on('onLogout', () => {
|
|
||||||
pass('Signed out');
|
|
||||||
|
|
||||||
dispatch(logoutUser());
|
|
||||||
});
|
|
||||||
|
|
||||||
jwtService.on('onAutoLogout', (message) => {
|
|
||||||
pass(message);
|
|
||||||
|
|
||||||
dispatch(logoutUser());
|
|
||||||
});
|
|
||||||
|
|
||||||
jwtService.on('onNoAccessToken', () => {
|
|
||||||
pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
jwtService.init();
|
|
||||||
|
|
||||||
function success(user, message) {
|
|
||||||
if (message) {
|
|
||||||
dispatch(showMessage({ message }));
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
dispatch(setUser(user)),
|
|
||||||
// You can receive data in here before app initialization
|
|
||||||
]).then((values) => {
|
|
||||||
setWaitAuthCheck(false);
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function pass(message) {
|
|
||||||
if (message) {
|
|
||||||
dispatch(showMessage({ message }));
|
|
||||||
}
|
|
||||||
|
|
||||||
setWaitAuthCheck(false);
|
|
||||||
setIsAuthenticated(false);
|
|
||||||
}
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return waitAuthCheck ? (
|
|
||||||
<FuseSplashScreen />
|
|
||||||
) : (
|
|
||||||
<AuthContext.Provider value={{ isAuthenticated }}>{children}</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useAuth() {
|
|
||||||
const context = React.useContext(AuthContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error('useAuth must be used within a AuthProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { AuthProvider, useAuth };
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/**
|
|
||||||
* Authorization Roles
|
|
||||||
*/
|
|
||||||
const authRoles = {
|
|
||||||
admin: ['admin'],
|
|
||||||
staff: ['admin', 'staff'],
|
|
||||||
user: ['admin', 'staff', 'user'],
|
|
||||||
onlyGuest: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default authRoles;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as authRoles } from './authRoles';
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import JwtService from './jwtService';
|
|
||||||
|
|
||||||
export default JwtService;
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
import FuseUtils from '@fuse/utils/FuseUtils';
|
|
||||||
import axios from 'axios';
|
|
||||||
import jwtDecode from 'jwt-decode';
|
|
||||||
import jwtServiceConfig from './jwtServiceConfig';
|
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
|
|
||||||
class JwtService extends FuseUtils.EventEmitter {
|
|
||||||
init() {
|
|
||||||
this.setInterceptors();
|
|
||||||
this.handleAuthentication();
|
|
||||||
}
|
|
||||||
|
|
||||||
setInterceptors = () => {
|
|
||||||
axios.interceptors.response.use(
|
|
||||||
(response) => {
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (err.response.status === 401 && err.config && !err.config.__isRetryRequest) {
|
|
||||||
// if you ever get an unauthorized response, logout the user
|
|
||||||
this.emit('onAutoLogout', 'Invalid access_token');
|
|
||||||
this.setSession(null);
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAuthentication = () => {
|
|
||||||
const access_token = this.getAccessToken();
|
|
||||||
|
|
||||||
if (!access_token) {
|
|
||||||
this.emit('onNoAccessToken');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isAuthTokenValid(access_token)) {
|
|
||||||
this.setSession(access_token);
|
|
||||||
this.emit('onAutoLogin', true);
|
|
||||||
} else {
|
|
||||||
this.setSession(null);
|
|
||||||
this.emit('onAutoLogout', 'access_token expired');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
createUser = (data) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
axios.post(jwtServiceConfig.signUp, data).then((response) => {
|
|
||||||
if (response.data.user) {
|
|
||||||
this.setSession(response.data.access_token);
|
|
||||||
resolve(response.data.user);
|
|
||||||
this.emit('onLogin', response.data.user);
|
|
||||||
} else {
|
|
||||||
reject(response.data.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
signInWithEmailAndPassword = (email, password) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
axios
|
|
||||||
.get(jwtServiceConfig.signIn, {
|
|
||||||
data: {
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (response.data.user) {
|
|
||||||
this.setSession(response.data.access_token);
|
|
||||||
resolve(response.data.user);
|
|
||||||
this.emit('onLogin', response.data.user);
|
|
||||||
} else {
|
|
||||||
reject(response.data.error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
signInWithToken = () => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
axios
|
|
||||||
.get(jwtServiceConfig.accessToken, {
|
|
||||||
data: {
|
|
||||||
access_token: this.getAccessToken(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (response.data.user) {
|
|
||||||
this.setSession(response.data.access_token);
|
|
||||||
resolve(response.data.user);
|
|
||||||
} else {
|
|
||||||
this.logout();
|
|
||||||
reject(new Error('Failed to login with token.'));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.logout();
|
|
||||||
reject(new Error('Failed to login with token.'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
updateUserData = (user) => {
|
|
||||||
return axios.post(jwtServiceConfig.updateUser, {
|
|
||||||
user,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setSession = (access_token) => {
|
|
||||||
if (access_token) {
|
|
||||||
localStorage.setItem('jwt_access_token', access_token);
|
|
||||||
axios.defaults.headers.common.Authorization = `Bearer ${access_token}`;
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('jwt_access_token');
|
|
||||||
delete axios.defaults.headers.common.Authorization;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
logout = () => {
|
|
||||||
this.setSession(null);
|
|
||||||
this.emit('onLogout', 'Logged out');
|
|
||||||
};
|
|
||||||
|
|
||||||
isAuthTokenValid = (access_token) => {
|
|
||||||
if (!access_token) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const decoded = jwtDecode(access_token);
|
|
||||||
const currentTime = Date.now() / 1000;
|
|
||||||
if (decoded.exp < currentTime) {
|
|
||||||
console.warn('access token expired');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
getAccessToken = () => {
|
|
||||||
return window.localStorage.getItem('jwt_access_token');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = new JwtService();
|
|
||||||
|
|
||||||
export default instance;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const jwtServiceConfig = {
|
|
||||||
signIn: 'api/auth/sign-in',
|
|
||||||
signUp: 'api/auth/sign-up',
|
|
||||||
accessToken: 'api/auth/access-token',
|
|
||||||
updateUser: 'api/auth/user/update',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default jwtServiceConfig;
|
|
||||||
19
src/app/configs/consts.js
Normal file
19
src/app/configs/consts.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export const STATISTICS_MODES = {
|
||||||
|
positive: 'positive',
|
||||||
|
extra_positive: 'extra_positive',
|
||||||
|
negative: 'negative',
|
||||||
|
extra_negative: 'extra_negative',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROPERTIES_LAYOUTS = {
|
||||||
|
list: 'list',
|
||||||
|
grid: 'grid',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Authorization Roles
|
||||||
|
export const authRoles = {
|
||||||
|
admin: ['admin'],
|
||||||
|
staff: ['admin', 'staff'],
|
||||||
|
user: ['admin', 'staff', 'user'],
|
||||||
|
onlyGuest: [],
|
||||||
|
};
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
const locale = {
|
|
||||||
APPLICATIONS: 'تطبيقات',
|
|
||||||
EXAMPLE: 'مثال',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default locale;
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
const locale = {
|
const locale = {
|
||||||
APPLICATIONS: 'Applications',
|
dashboard: 'Dashboard',
|
||||||
EXAMPLE: 'Example',
|
favorites: 'Favorites',
|
||||||
|
history: 'History',
|
||||||
|
profile: 'My profile',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default locale;
|
export default locale;
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
const locale = {
|
|
||||||
APPLICATIONS: 'Programlar',
|
|
||||||
EXAMPLE: 'Örnek Sayfa',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default locale;
|
|
||||||
@@ -1,20 +1,36 @@
|
|||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import ar from './navigation-i18n/ar';
|
|
||||||
import en from './navigation-i18n/en';
|
import en from './navigation-i18n/en';
|
||||||
import tr from './navigation-i18n/tr';
|
|
||||||
|
|
||||||
i18next.addResourceBundle('en', 'navigation', en);
|
i18next.addResourceBundle('en', 'navigation', en);
|
||||||
i18next.addResourceBundle('tr', 'navigation', tr);
|
|
||||||
i18next.addResourceBundle('ar', 'navigation', ar);
|
|
||||||
|
|
||||||
const navigationConfig = [
|
const navigationConfig = [
|
||||||
{
|
{
|
||||||
id: 'example-component',
|
id: 'dashboard',
|
||||||
title: 'Example',
|
title: en.dashboard,
|
||||||
translate: 'EXAMPLE',
|
|
||||||
type: 'item',
|
type: 'item',
|
||||||
icon: 'heroicons-outline:star',
|
icon: 'heroicons-outline:view-grid',
|
||||||
url: 'example',
|
url: 'dashboard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'favorites',
|
||||||
|
title: en.favorites,
|
||||||
|
type: 'item',
|
||||||
|
icon: 'heroicons-outline:heart',
|
||||||
|
url: 'favorites',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'history',
|
||||||
|
title: en.history,
|
||||||
|
type: 'item',
|
||||||
|
icon: 'heroicons-outline:archive',
|
||||||
|
url: 'history',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'profile',
|
||||||
|
title: en.profile,
|
||||||
|
type: 'item',
|
||||||
|
icon: 'heroicons-outline:user-circle',
|
||||||
|
url: 'profile',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,16 @@ import FuseUtils from '@fuse/utils';
|
|||||||
import FuseLoading from '@fuse/core/FuseLoading';
|
import FuseLoading from '@fuse/core/FuseLoading';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import settingsConfig from 'app/configs/settingsConfig';
|
import settingsConfig from 'app/configs/settingsConfig';
|
||||||
import SignInConfig from '../main/sign-in/SignInConfig';
|
|
||||||
import SignUpConfig from '../main/sign-up/SignUpConfig';
|
|
||||||
import SignOutConfig from '../main/sign-out/SignOutConfig';
|
|
||||||
import Error404Page from '../main/404/Error404Page';
|
import Error404Page from '../main/404/Error404Page';
|
||||||
import ExampleConfig from '../main/example/ExampleConfig';
|
import navigationPagesConfigs from '../main/navigationPages/navigationPagesConfig';
|
||||||
|
import authPagesConfigs from '../main/authPages/authPagesConfigs';
|
||||||
|
import HomeConfig from '../main/home/HomeConfig';
|
||||||
|
import RentAndBuyConfig from '../main/rentAndBuy/RentAndBuyConfig';
|
||||||
|
|
||||||
const routeConfigs = [ExampleConfig, SignOutConfig, SignInConfig, SignUpConfig];
|
const routeConfigs = [...navigationPagesConfigs, ...authPagesConfigs, HomeConfig, RentAndBuyConfig];
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
...FuseUtils.generateRoutesFromConfigs(routeConfigs, settingsConfig.defaultAuth),
|
...FuseUtils.generateRoutesFromConfigs(routeConfigs, settingsConfig.defaultAuth),
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
element: <Navigate to="/example" />,
|
|
||||||
auth: settingsConfig.defaultAuth,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'loading',
|
path: 'loading',
|
||||||
element: <FuseLoading />,
|
element: <FuseLoading />,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const settingsConfig = {
|
|||||||
To make whole app accessible without authorization by default set defaultAuth: null
|
To make whole app accessible without authorization by default set defaultAuth: null
|
||||||
*** The individual route configs which has auth option won't be overridden.
|
*** The individual route configs which has auth option won't be overridden.
|
||||||
*/
|
*/
|
||||||
defaultAuth: ['admin'],
|
defaultAuth: ['admin', 'staff', 'user'],
|
||||||
/*
|
/*
|
||||||
Default redirect url for the logged-in user,
|
Default redirect url for the logged-in user,
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { fuseDark, skyBlue } from '@fuse/colors';
|
|
||||||
import { blueGrey } from '@mui/material/colors';
|
|
||||||
|
|
||||||
export const lightPaletteText = {
|
export const lightPaletteText = {
|
||||||
primary: 'rgb(17, 24, 39)',
|
primary: '#151B30',
|
||||||
secondary: 'rgb(107, 114, 128)',
|
secondary: '#6D6D6D',
|
||||||
disabled: 'rgb(149, 156, 169)',
|
disabled: '#D9D9D9',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const darkPaletteText = {
|
export const darkPaletteText = {
|
||||||
@@ -22,32 +19,35 @@ const themesConfig = {
|
|||||||
common: {
|
common: {
|
||||||
black: 'rgb(17, 24, 39)',
|
black: 'rgb(17, 24, 39)',
|
||||||
white: 'rgb(255, 255, 255)',
|
white: 'rgb(255, 255, 255)',
|
||||||
|
layout: '#141D39',
|
||||||
|
...lightPaletteText,
|
||||||
},
|
},
|
||||||
primary: {
|
primary: {
|
||||||
light: '#64748b',
|
light: '#64748b',
|
||||||
main: '#1e293b',
|
main: '#F1F5F9',
|
||||||
dark: '#0f172a',
|
dark: '#F1F1FB',
|
||||||
contrastText: darkPaletteText.primary,
|
contrastText: darkPaletteText.primary,
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
light: '#818cf8',
|
light: '#1AD079',
|
||||||
main: '#4f46e5',
|
main: '#4D53FF',
|
||||||
dark: '#3730a3',
|
dark: '#3730a3',
|
||||||
contrastText: darkPaletteText.primary,
|
contrastText: darkPaletteText.primary,
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
paper: '#FFFFFF',
|
paper: '#FFFFFF',
|
||||||
default: '#f1f5f9',
|
default: '#F1F5F9',
|
||||||
|
},
|
||||||
|
accept: {
|
||||||
|
light: '#E8FAF2',
|
||||||
|
main: '#10A75F',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
light: '#ffcdd2',
|
light: '#FBEBEA',
|
||||||
main: '#f44336',
|
main: '#D83529',
|
||||||
dark: '#b71c1c',
|
dark: '#b71c1c',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
defaultDark: {
|
defaultDark: {
|
||||||
palette: {
|
palette: {
|
||||||
@@ -72,7 +72,7 @@ const themesConfig = {
|
|||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
paper: '#1e293b',
|
paper: '#1e293b',
|
||||||
default: '#111827',
|
default: '#141D39',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
light: '#ffcdd2',
|
light: '#ffcdd2',
|
||||||
@@ -84,816 +84,6 @@ const themesConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
legacy: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
common: {
|
|
||||||
black: 'rgb(17, 24, 39)',
|
|
||||||
white: 'rgb(255, 255, 255)',
|
|
||||||
},
|
|
||||||
primary: {
|
|
||||||
light: fuseDark[200],
|
|
||||||
main: fuseDark[500],
|
|
||||||
dark: fuseDark[800],
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: skyBlue[100],
|
|
||||||
main: skyBlue[500],
|
|
||||||
dark: skyBlue[900],
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#FFFFFF',
|
|
||||||
default: '#f6f7f9',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
light1: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#b3d1d1',
|
|
||||||
main: '#006565',
|
|
||||||
dark: '#003737',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#ffecc0',
|
|
||||||
main: '#FFBE2C',
|
|
||||||
dark: '#ff9910',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#FFFFFF',
|
|
||||||
default: '#F0F7F7',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
light2: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#fdf3da',
|
|
||||||
main: '#f8d683',
|
|
||||||
dark: '#f3bc53',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#FADCB3',
|
|
||||||
main: '#F3B25F',
|
|
||||||
dark: '#ec9339',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#FAFBFD',
|
|
||||||
default: '#FFFFFF',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
light3: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#D9C8CE',
|
|
||||||
main: '#80485B',
|
|
||||||
dark: '#50212F',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#FFE3BF',
|
|
||||||
main: '#FFB049',
|
|
||||||
dark: '#FF8619',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#FFF0DF',
|
|
||||||
default: '#FAFAFE',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
light4: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#CDCCE8',
|
|
||||||
main: '#5854B1',
|
|
||||||
dark: '#2D2988',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#F8EBF2',
|
|
||||||
main: '#E7BDD3',
|
|
||||||
dark: '#D798B7',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#FFFFFF',
|
|
||||||
default: '#F6F7FB',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
light5: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#C2C7F1',
|
|
||||||
main: '#3543D0',
|
|
||||||
dark: '#161EB3',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#B3F1FE',
|
|
||||||
main: '#00CFFD',
|
|
||||||
dark: '#00B2FC',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#FFFFFF',
|
|
||||||
default: '#F7FAFF',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
light6: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#BBE2DA',
|
|
||||||
main: '#1B9E85',
|
|
||||||
dark: '#087055',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#FFD0C1',
|
|
||||||
main: '#FF6231',
|
|
||||||
dark: '#FF3413',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#FFFFFF',
|
|
||||||
default: '#F2F8F1',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
light7: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#BFC4E6',
|
|
||||||
main: '#2A3BAB',
|
|
||||||
dark: '#0F1980',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#C2ECF0',
|
|
||||||
main: '#33C1CD',
|
|
||||||
dark: '#149EAE',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#FFFFFF',
|
|
||||||
default: '#EDF0F6',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
light8: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#D2EFF2',
|
|
||||||
main: '#68C8D5',
|
|
||||||
dark: '#3AA7BA',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#FFF2C6',
|
|
||||||
main: '#FED441',
|
|
||||||
dark: '#FDB91C',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#FAF6F3',
|
|
||||||
default: '#FFFFFF',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
light9: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#D3C0CD',
|
|
||||||
main: '#6B2C57',
|
|
||||||
dark: '#3C102C',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#FDEAC9',
|
|
||||||
main: '#F9B84B',
|
|
||||||
dark: '#F59123',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#FFFFFF',
|
|
||||||
default: '#FAFAFE',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
light10: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#C6C9CD',
|
|
||||||
main: '#404B57',
|
|
||||||
dark: '#1C232C',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#FEEDC7',
|
|
||||||
main: '#FCC344',
|
|
||||||
dark: '#FAA11F',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#FFFFFF',
|
|
||||||
default: '#F5F4F6',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
light11: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#C4C4C4',
|
|
||||||
main: '#3A3A3A',
|
|
||||||
dark: '#181818',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#EFEFED',
|
|
||||||
main: '#CBCAC3',
|
|
||||||
dark: '#ACABA1',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#EFEEE7',
|
|
||||||
default: '#FAF8F2',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#F7EAEA',
|
|
||||||
main: '#EBCECE',
|
|
||||||
dark: '#E3B9B9',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'yellow',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
light12: {
|
|
||||||
palette: {
|
|
||||||
mode: 'light',
|
|
||||||
divider: '#e2e8f0',
|
|
||||||
text: lightPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#FFFAF6',
|
|
||||||
main: '#FFEDE2',
|
|
||||||
dark: '#FFE0CF',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#DBD8F7',
|
|
||||||
main: '#887CE3',
|
|
||||||
dark: '#584CD0',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#FFFFFF',
|
|
||||||
default: '#FCF8F5',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark1: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#C2C2C3',
|
|
||||||
main: '#323338',
|
|
||||||
dark: '#131417',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#B8E1D9',
|
|
||||||
main: '#129B7F',
|
|
||||||
dark: '#056D4F',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#262526',
|
|
||||||
default: '#1E1D1E',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark2: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#C9CACE',
|
|
||||||
main: '#4B4F5A',
|
|
||||||
dark: '#23262E',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#F8F5F2',
|
|
||||||
main: '#E6DED5',
|
|
||||||
dark: '#D5C8BA',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#31343E',
|
|
||||||
default: '#2A2D35',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#F7EAEA',
|
|
||||||
main: '#EBCECE',
|
|
||||||
dark: '#E3B9B9',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark3: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#C2C8D2',
|
|
||||||
main: '#354968',
|
|
||||||
dark: '#16213A',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#F4CFCA',
|
|
||||||
main: '#D55847',
|
|
||||||
dark: '#C03325',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#23354E',
|
|
||||||
default: '#1B2A3F',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark4: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#CECADF',
|
|
||||||
main: '#5A4E93',
|
|
||||||
dark: '#2E2564',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#B3EBD6',
|
|
||||||
main: '#00BC77',
|
|
||||||
dark: '#009747',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#22184B',
|
|
||||||
default: '#180F3D',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark5: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#CCD7E2',
|
|
||||||
main: '#56789D',
|
|
||||||
dark: '#2B486F',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#D7D3ED',
|
|
||||||
main: '#796CC4',
|
|
||||||
dark: '#493DA2',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#465261',
|
|
||||||
default: '#232931',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark6: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#FFC7CE',
|
|
||||||
main: '#FF445D',
|
|
||||||
dark: '#FF1F30',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#B4E3FB',
|
|
||||||
main: '#05A2F3',
|
|
||||||
dark: '#0175EA',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#2F3438',
|
|
||||||
default: '#25292E',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark7: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: 'FFECC5',
|
|
||||||
main: '#FEBE3E',
|
|
||||||
dark: '#FD991B',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#FFC8C7',
|
|
||||||
main: '#FE4644',
|
|
||||||
dark: '#FD201F',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#2A2E32',
|
|
||||||
default: '#212529',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark8: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#BEBFC8',
|
|
||||||
main: '#252949',
|
|
||||||
dark: '#0D0F21',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#CBD7FE',
|
|
||||||
main: '#5079FC',
|
|
||||||
dark: '#2749FA',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#2D3159',
|
|
||||||
default: '#202441',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark9: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#BCC8CD',
|
|
||||||
main: '#204657',
|
|
||||||
dark: '#0B202C',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#B3EBC5',
|
|
||||||
main: '#00BD3E',
|
|
||||||
dark: '#00981B',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#1C1E27',
|
|
||||||
default: '#15171E',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark10: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#C3C2D2',
|
|
||||||
main: '#36336A',
|
|
||||||
dark: '#16143C',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#D6CEFC',
|
|
||||||
main: '#765CF5',
|
|
||||||
dark: '#4630EE',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#2D2A5D',
|
|
||||||
default: '#26244E',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark11: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#BFB7BF',
|
|
||||||
main: '#2A0F29',
|
|
||||||
dark: '#0F040F',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#D9B9C3',
|
|
||||||
main: '#801737',
|
|
||||||
dark: '#500716',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#200D1F',
|
|
||||||
default: '#2D132C',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dark12: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: '#CCC3C8',
|
|
||||||
main: '#543847',
|
|
||||||
dark: '#291720',
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: '#DFB8BD',
|
|
||||||
main: '#BE717A',
|
|
||||||
dark: '#99424A',
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: '#4D4351',
|
|
||||||
default: '#27141F',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
greyDark: {
|
|
||||||
palette: {
|
|
||||||
mode: 'dark',
|
|
||||||
divider: 'rgba(241,245,249,.12)',
|
|
||||||
text: darkPaletteText,
|
|
||||||
primary: {
|
|
||||||
light: fuseDark[200],
|
|
||||||
main: fuseDark[700],
|
|
||||||
dark: fuseDark[800],
|
|
||||||
contrastText: darkPaletteText.primary,
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
light: skyBlue[100],
|
|
||||||
main: skyBlue[500],
|
|
||||||
dark: skyBlue[900],
|
|
||||||
contrastText: lightPaletteText.primary,
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
paper: blueGrey[700],
|
|
||||||
default: blueGrey[900],
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
light: '#ffcdd2',
|
|
||||||
main: '#f44336',
|
|
||||||
dark: '#b71c1c',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
danger: 'orange',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default themesConfig;
|
export default themesConfig;
|
||||||
|
|||||||
147
src/app/contexts/AuthContext.js
Normal file
147
src/app/contexts/AuthContext.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import FuseSplashScreen from '@fuse/core/FuseSplashScreen';
|
||||||
|
import { showMessage } from 'app/store/fuse/messageSlice';
|
||||||
|
import { logoutUser, setUser } from 'app/store/userSlice';
|
||||||
|
import { authService, firebase } from '../services';
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
image:
|
||||||
|
'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80',
|
||||||
|
title: '6 Via delle Crosarolle, Crosarolle, Veneto, Crosarolle, Veneto',
|
||||||
|
category: 'buy',
|
||||||
|
status: 'new',
|
||||||
|
favorite: false,
|
||||||
|
update: '12.12.2022',
|
||||||
|
statistics: [
|
||||||
|
{ subject: 'Monthly Cash Flow', value: '$ 78,000', mode: 'extra_positive' },
|
||||||
|
{ subject: 'Cash on Cash Return', value: '78%', mode: 'positive' },
|
||||||
|
{ subject: 'Selling Prise', value: '$500,000', mode: '' },
|
||||||
|
{ subject: 'Cash Out of Pocket', value: '$125,000', mode: '' },
|
||||||
|
{ subject: 'Annual Revenue', value: '$5,000', mode: '' },
|
||||||
|
{ subject: 'Annual Net Profit', value: '+$50,000', mode: 'extra_positive' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '456',
|
||||||
|
image:
|
||||||
|
'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80',
|
||||||
|
title: '6 Via delle Crosarolle, Crosarolle, Veneto',
|
||||||
|
category: 'buy',
|
||||||
|
status: 'new',
|
||||||
|
favorite: false,
|
||||||
|
update: '12.12.2022',
|
||||||
|
statistics: [
|
||||||
|
{ subject: 'Monthly Cash Flow', value: '$ 78,000', mode: 'extra_negative' },
|
||||||
|
{ subject: 'Cash on Cash Return', value: '78%', mode: 'negative' },
|
||||||
|
{ subject: 'Selling Prise', value: '$500,000', mode: '' },
|
||||||
|
{ subject: 'Cash Out of Pocket', value: '$125,000', mode: '' },
|
||||||
|
{ subject: 'Annual Revenue', value: '$5,000', mode: '' },
|
||||||
|
{ subject: 'Annual Net Profit', value: '+$50,000', mode: 'extra_negative' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '789',
|
||||||
|
image:
|
||||||
|
'https://images.unsplash.com/photo-1600585154340-be6161a56a0c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80',
|
||||||
|
title: '6 Via delle Crosarolle, Crosarolle, Veneto',
|
||||||
|
category: 'rent',
|
||||||
|
status: 'new',
|
||||||
|
favorite: false,
|
||||||
|
update: '12.12.2022',
|
||||||
|
statistics: [
|
||||||
|
{ subject: 'Monthly Cash Flow', value: '$ 78,000', mode: 'positive' },
|
||||||
|
{ subject: 'Cash on Cash Return', value: '78%', mode: 'positive' },
|
||||||
|
{ subject: 'Selling Prise', value: '$500,000', mode: '' },
|
||||||
|
{ subject: 'Cash Out of Pocket', value: '$125,000', mode: '' },
|
||||||
|
{ subject: 'Annual Revenue', value: '$5,000', mode: '' },
|
||||||
|
{ subject: 'Annual Net Profit', value: '+$50,000', mode: 'extra_positive' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const AuthContext = React.createContext();
|
||||||
|
|
||||||
|
function AuthProvider({ children }) {
|
||||||
|
const [isAuthenticated, setIsAuthenticated] = useState(undefined);
|
||||||
|
const [waitAuthCheck, setWaitAuthCheck] = useState(true);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
authService.on('onLogout', () => {
|
||||||
|
pass('Signed out');
|
||||||
|
|
||||||
|
dispatch(logoutUser());
|
||||||
|
});
|
||||||
|
|
||||||
|
authService.init(firebase.auth, firebase.db);
|
||||||
|
|
||||||
|
authService.onAuthStateChanged((authUser) => {
|
||||||
|
dispatch(showMessage({ message: 'Signing...' }));
|
||||||
|
if (authUser) {
|
||||||
|
const storageUser = JSON.parse(localStorage.user ?? '{}');
|
||||||
|
authService
|
||||||
|
.getUserData(authUser.uid)
|
||||||
|
.then((user) => {
|
||||||
|
if (user) {
|
||||||
|
success({ ...user, ...storageUser }, 'Signed in');
|
||||||
|
} else {
|
||||||
|
// First login
|
||||||
|
const { displayName, photoURL, email } = authUser;
|
||||||
|
success(
|
||||||
|
{ role: 'user', data: { displayName, photoURL, email }, ...storageUser },
|
||||||
|
'Signed in'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
pass(error.message);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pass('Signed out');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function success(user, message) {
|
||||||
|
if (message) {
|
||||||
|
dispatch(showMessage({ message }));
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
dispatch(setUser(user)),
|
||||||
|
// You can receive data in here before app initialization
|
||||||
|
]).then((values) => {
|
||||||
|
setWaitAuthCheck(false);
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pass(message) {
|
||||||
|
if (message) {
|
||||||
|
dispatch(showMessage({ message }));
|
||||||
|
}
|
||||||
|
|
||||||
|
setWaitAuthCheck(false);
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
}
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return waitAuthCheck ? (
|
||||||
|
<FuseSplashScreen />
|
||||||
|
) : (
|
||||||
|
<AuthContext.Provider value={{ isAuthenticated }}>{children}</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAuth() {
|
||||||
|
const context = React.useContext(AuthContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAuth must be used within a AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AuthProvider, useAuth };
|
||||||
3
src/app/hooks/index.js
Normal file
3
src/app/hooks/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as useWindowDimensions } from './useWindowDimensions';
|
||||||
|
export { default as useOnClickOutside } from './useOnClickOutside';
|
||||||
|
export { default as usePropertiesHeader } from './usePropertiesHeader';
|
||||||
29
src/app/hooks/useOnClickOutside.js
Normal file
29
src/app/hooks/useOnClickOutside.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function useOnClickOutside(ref, handler) {
|
||||||
|
useEffect(() => {
|
||||||
|
const onEvent = (event) => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event instanceof KeyboardEvent && event.key === 'Escape') {
|
||||||
|
handler(event);
|
||||||
|
} else if (ref.current.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', onEvent);
|
||||||
|
document.addEventListener('touchstart', onEvent);
|
||||||
|
document.addEventListener('keydown', onEvent);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', onEvent);
|
||||||
|
document.removeEventListener('touchstart', onEvent);
|
||||||
|
document.removeEventListener('keydown', onEvent);
|
||||||
|
};
|
||||||
|
}, [ref, handler]);
|
||||||
|
}
|
||||||
105
src/app/hooks/usePropertiesHeader.js
Normal file
105
src/app/hooks/usePropertiesHeader.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { PROPERTIES_LAYOUTS } from 'app/configs/consts';
|
||||||
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function usePropertiesHeader(items) {
|
||||||
|
const [categories, setCategories] = useState([
|
||||||
|
{
|
||||||
|
name: 'all',
|
||||||
|
amount: 0,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const [layouts, setLayouts] = useState([
|
||||||
|
{ name: PROPERTIES_LAYOUTS.list, active: true },
|
||||||
|
{ name: PROPERTIES_LAYOUTS.grid, active: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const activeCategory = useMemo(
|
||||||
|
() => categories.find(({ active }) => active)?.name ?? categories[0].name,
|
||||||
|
[categories]
|
||||||
|
);
|
||||||
|
const activeLayout = useMemo(
|
||||||
|
() => layouts.find(({ active }) => active)?.name ?? PROPERTIES_LAYOUTS.list,
|
||||||
|
[layouts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onCategory = useCallback(
|
||||||
|
(value) => {
|
||||||
|
if (value === activeCategory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCategories((prevState) =>
|
||||||
|
prevState.map((category) => ({ ...category, active: category.name === value }))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[activeCategory]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onLayout = useCallback(
|
||||||
|
(value) => {
|
||||||
|
if (value === activeLayout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayouts((prevState) => prevState.map(({ name }) => ({ name, active: name === value })));
|
||||||
|
},
|
||||||
|
[activeLayout]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onItemDelete = (itemCategory) => {
|
||||||
|
setCategories((prevState) => {
|
||||||
|
const isItemCategoryLast =
|
||||||
|
prevState.find((category) => category.name === itemCategory)?.amount === 1;
|
||||||
|
|
||||||
|
return prevState
|
||||||
|
.map((category, idx) => {
|
||||||
|
if (!idx) {
|
||||||
|
return {
|
||||||
|
name: category.name,
|
||||||
|
amount: category.amount - 1,
|
||||||
|
active: isItemCategoryLast,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category?.name === itemCategory) {
|
||||||
|
if (category.amount > 1) {
|
||||||
|
return { ...category, amount: category.amount - 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return category;
|
||||||
|
})
|
||||||
|
.filter((category) => category);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let updatedCategories = [...categories];
|
||||||
|
items.forEach((item) => {
|
||||||
|
const hasItemCategory = updatedCategories.find((category) => category.name === item.category);
|
||||||
|
updatedCategories = updatedCategories.map((category, idx) => {
|
||||||
|
if (!idx) {
|
||||||
|
category.amount += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return category;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasItemCategory) {
|
||||||
|
updatedCategories = updatedCategories.map((category) => ({
|
||||||
|
...category,
|
||||||
|
amount: item.category === category.name ? category.amount + 1 : category.amount,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
updatedCategories.push({ name: item.category, amount: 1, active: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setCategories(updatedCategories);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { categories, activeCategory, onCategory, layouts, activeLayout, onLayout, onItemDelete };
|
||||||
|
}
|
||||||
20
src/app/hooks/useWindowDimensions.js
Normal file
20
src/app/hooks/useWindowDimensions.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function useWindowDimensions() {
|
||||||
|
const getWindowDimensions = () => {
|
||||||
|
const { innerWidth: width, innerHeight: height } = window;
|
||||||
|
|
||||||
|
return { width, height };
|
||||||
|
};
|
||||||
|
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onRecize = () => setWindowDimensions(getWindowDimensions());
|
||||||
|
|
||||||
|
window.addEventListener('resize', onRecize);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('resize', onRecize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return windowDimensions;
|
||||||
|
}
|
||||||
8
src/app/main/authPages/authPagesConfigs.js
Normal file
8
src/app/main/authPages/authPagesConfigs.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import ForgotPasswordConfig from './forgot-password/ForgotPasswordConfig';
|
||||||
|
import SignInConfig from './sign-in/SignInConfig';
|
||||||
|
import SignOutConfig from './sign-out/SignOutConfig';
|
||||||
|
import SignUpConfig from './sign-up/SignUpConfig';
|
||||||
|
|
||||||
|
const authPagesConfigs = [SignInConfig, SignOutConfig, SignUpConfig, ForgotPasswordConfig];
|
||||||
|
|
||||||
|
export default authPagesConfigs;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
|
import ForgotPasswordPage from './ForgotPasswordPage';
|
||||||
|
import { authRoles } from '../../../configs/consts';
|
||||||
|
import en from './i18n/en';
|
||||||
|
|
||||||
|
i18next.addResourceBundle('en', 'forgotPasswordPage', en);
|
||||||
|
|
||||||
|
const ForgotPasswordConfig = {
|
||||||
|
settings: {
|
||||||
|
layout: {
|
||||||
|
config: {
|
||||||
|
navbar: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: authRoles.onlyGuest,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: 'forgot-password',
|
||||||
|
element: <ForgotPasswordPage />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ForgotPasswordConfig;
|
||||||
123
src/app/main/authPages/forgot-password/ForgotPasswordPage.js
Normal file
123
src/app/main/authPages/forgot-password/ForgotPasswordPage.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import _ from '@lodash';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { authService } from 'src/app/services';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import LeftSideCanvas from '../shared-components/LeftSideCanvas';
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
email: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledTextField = forwardRef((props, ref) => (
|
||||||
|
<TextField
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
function ForgotPasswordPage({ t }) {
|
||||||
|
const schema = yup.object().shape({
|
||||||
|
email: yup.string().email(t('email_error')).required(t('email_error')),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { control, formState, handleSubmit, setError } = useForm({
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues,
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isValid, dirtyFields, errors } = formState;
|
||||||
|
|
||||||
|
function onSubmit({ email }) {
|
||||||
|
authService.sendPasswordResetEmail(email).catch((error) => {
|
||||||
|
setError('root', {
|
||||||
|
type: 'manual',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-auto min-w-0">
|
||||||
|
<LeftSideCanvas title={t('title')} subtitle={t('subtitle')} text={t('text')} />
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
className="h-full w-full sm:h-auto md:flex md:h-full py-32 px-16 sm:p-48 md:p-40 md:px-96 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none rtl:border-r-1 ltr:border-l-1"
|
||||||
|
sx={{ background: (theme) => theme.palette.background?.default }}
|
||||||
|
>
|
||||||
|
<div className="w-full mx-auto sm:mx-0">
|
||||||
|
<Typography className="text-4xl font-extrabold tracking-tight leading-tight">
|
||||||
|
{t('forgot_password')}
|
||||||
|
</Typography>
|
||||||
|
<div className="flex items-baseline mt-10 font-medium">
|
||||||
|
<Typography>{t('suggestion')}</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
name="forgotPasswordForm"
|
||||||
|
noValidate
|
||||||
|
className="flex flex-col justify-center w-full mt-48"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="email"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
label={t('email')}
|
||||||
|
type="email"
|
||||||
|
error={!!errors.email}
|
||||||
|
helperText={errors?.email?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center justify-center gap-10 w-full">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
className="w-[300px] mt-32 text-base uppercase rounded-xl"
|
||||||
|
aria-label="Register"
|
||||||
|
disabled={_.isEmpty(dirtyFields) || !isValid}
|
||||||
|
type="submit"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{t('forgot_password_btn')}
|
||||||
|
</Button>
|
||||||
|
{errors.root?.message && (
|
||||||
|
<p className="text-l text-error-main">{errors.root?.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Typography className="mt-32 font-medium" color="text.secondary">
|
||||||
|
<span>{t('return')}</span>
|
||||||
|
<Link className="ml-4 text-secondary-main underline" to="/sign-in">
|
||||||
|
{t('sign_in')}
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('forgotPasswordPage')(ForgotPasswordPage);
|
||||||
16
src/app/main/authPages/forgot-password/i18n/en.js
Normal file
16
src/app/main/authPages/forgot-password/i18n/en.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const locale = {
|
||||||
|
title: 'Lorem ipsum dolor sit amet!',
|
||||||
|
subtitle:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Scelerisque blandit sit sagittis justo viverra. Morbi accumsaniam elementum enim commodo sed mauris vel. Scelerisque rhoncus in metus non arcu cursus non rhoncus.',
|
||||||
|
text: 'Lorem ipsum dolor sit amet consectetur. Scelerisque blandit sit.',
|
||||||
|
forgot_password: 'Forgot password?',
|
||||||
|
suggestion: 'Fill the form to reset your password',
|
||||||
|
email: 'Email',
|
||||||
|
email_error:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
|
||||||
|
forgot_password_btn: 'send reset link',
|
||||||
|
return: 'Return to',
|
||||||
|
sign_in: 'Sign in',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default locale;
|
||||||
62
src/app/main/authPages/shared-components/LeftSideCanvas.js
Normal file
62
src/app/main/authPages/shared-components/LeftSideCanvas.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import AvatarGroup from '@mui/material/AvatarGroup';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
|
function LeftSideCanvas({ title, subtitle, text }) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className="h-full min-h-screen relative hidden md:flex flex-auto items-center justify-center p-64 lg:px-112 overflow-hidden max-w-[45vw] w-full"
|
||||||
|
sx={{ backgroundColor: 'common.layout' }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="absolute inset-0 pointer-events-none"
|
||||||
|
viewBox="0 0 960 540"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
preserveAspectRatio="xMidYMax slice"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="g"
|
||||||
|
sx={{ color: 'primary.light' }}
|
||||||
|
className="opacity-20"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="100"
|
||||||
|
>
|
||||||
|
<circle r="234" cx="196" cy="23" />
|
||||||
|
<circle r="234" cx="790" cy="491" />
|
||||||
|
</Box>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div className="z-10 relative w-full max-w-2xl">
|
||||||
|
{title && <div className="text-7xl font-bold leading-none text-primary-light">{title}</div>}
|
||||||
|
{subtitle && (
|
||||||
|
<div className="mt-24 text-lg tracking-tight leading-6 text-common-disabled">
|
||||||
|
{subtitle}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center mt-32">
|
||||||
|
<AvatarGroup
|
||||||
|
sx={{
|
||||||
|
'& .MuiAvatar-root': {
|
||||||
|
borderColor: 'common.layout',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar src="assets/images/avatars/female-18.jpg" />
|
||||||
|
<Avatar src="assets/images/avatars/female-11.jpg" />
|
||||||
|
<Avatar src="assets/images/avatars/male-09.jpg" />
|
||||||
|
<Avatar src="assets/images/avatars/male-16.jpg" />
|
||||||
|
</AvatarGroup>
|
||||||
|
|
||||||
|
{text && (
|
||||||
|
<div className="ml-16 font-medium tracking-tight text-common-disabled">{text}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LeftSideCanvas;
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
import SignInPage from './SignInPage';
|
import SignInPage from './SignInPage';
|
||||||
import authRoles from '../../auth/authRoles';
|
import { authRoles } from '../../../configs/consts';
|
||||||
|
import en from './i18n/en';
|
||||||
|
|
||||||
|
i18next.addResourceBundle('en', 'signInPage', en);
|
||||||
|
|
||||||
const SignInConfig = {
|
const SignInConfig = {
|
||||||
settings: {
|
settings: {
|
||||||
@@ -8,18 +13,6 @@ const SignInConfig = {
|
|||||||
navbar: {
|
navbar: {
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
toolbar: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
leftSidePanel: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
rightSidePanel: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
171
src/app/main/authPages/sign-in/SignInPage.js
Normal file
171
src/app/main/authPages/sign-in/SignInPage.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import _ from '@lodash';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { authService } from 'src/app/services';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import LeftSideCanvas from '../shared-components/LeftSideCanvas';
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
remember: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledTextField = forwardRef((props, ref) => (
|
||||||
|
<TextField
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
function SignInPage({ t }) {
|
||||||
|
const schema = yup.object().shape({
|
||||||
|
email: yup.string().email(t('email_error')).required(t('email_error')),
|
||||||
|
password: yup.string().required(t('password_error')).min(8, t('password_error')),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { control, formState, handleSubmit, setError } = useForm({
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues,
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isValid, dirtyFields, errors } = formState;
|
||||||
|
|
||||||
|
function onSubmit(data) {
|
||||||
|
authService.signInWithEmailAndPassword(data).catch((error) => {
|
||||||
|
setError('root', {
|
||||||
|
type: 'manual',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-auto min-w-0">
|
||||||
|
<LeftSideCanvas title={t('title')} subtitle={t('subtitle')} text={t('text')} />
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
className="h-full w-full sm:h-auto md:flex md:h-full py-32 px-16 sm:p-48 md:p-40 md:px-96 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none rtl:border-r-1 ltr:border-l-1"
|
||||||
|
sx={{ background: (theme) => theme.palette.background?.default }}
|
||||||
|
>
|
||||||
|
<div className="w-full mx-auto sm:mx-0">
|
||||||
|
<Typography className="text-4xl font-extrabold tracking-tight leading-tight">
|
||||||
|
Sign in
|
||||||
|
</Typography>
|
||||||
|
<div className="flex items-baseline mt-10 font-medium">
|
||||||
|
<Typography>{t('have_account')}</Typography>
|
||||||
|
<Link className="ml-4 text-secondary-main underline" to="/sign-up">
|
||||||
|
{t('sign_up')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
name="signinForm"
|
||||||
|
noValidate
|
||||||
|
className="flex flex-col justify-center w-full mt-48"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="email"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
className="mb-28"
|
||||||
|
label={t('email')}
|
||||||
|
autoFocus
|
||||||
|
type="email"
|
||||||
|
error={!!errors.email}
|
||||||
|
helperText={errors?.email?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="password"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
className="mb-28"
|
||||||
|
label={t('password')}
|
||||||
|
type="password"
|
||||||
|
error={!!errors.password}
|
||||||
|
helperText={errors?.password?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-center sm:justify-between">
|
||||||
|
<Controller
|
||||||
|
name="remember"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl>
|
||||||
|
<FormControlLabel
|
||||||
|
label={t('remember')}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
{...field}
|
||||||
|
sx={{
|
||||||
|
color: (theme) => theme.palette.common.disabled,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Link className="text-secondary-main underline font-medium" to="/forgot-password">
|
||||||
|
{t('forgot_password')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center justify-center gap-10 w-full">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
className="w-[220px] mt-32 text-base uppercase rounded-xl"
|
||||||
|
aria-label={t('sign_in_btn')}
|
||||||
|
disabled={_.isEmpty(dirtyFields) || !isValid}
|
||||||
|
type="submit"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{t('sign_in_btn')}
|
||||||
|
</Button>
|
||||||
|
{errors.root?.message && (
|
||||||
|
<p className="text-l text-error-main">{errors.root?.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('signInPage')(SignInPage);
|
||||||
23
src/app/main/authPages/sign-in/i18n/en.js
Normal file
23
src/app/main/authPages/sign-in/i18n/en.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const locale = {
|
||||||
|
title: 'Lorem ipsum dolor sit amet!',
|
||||||
|
subtitle:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Scelerisque blandit sit sagittis justo viverra. Morbi accumsaniam elementum enim commodo sed mauris vel. Scelerisque rhoncus in metus non arcu cursus non rhoncus.',
|
||||||
|
text: 'Lorem ipsum dolor sit amet consectetur. Scelerisque blandit sit.',
|
||||||
|
sign_in: 'Sign In',
|
||||||
|
have_account: 'Don`t have an account?',
|
||||||
|
sign_up: 'Sign up',
|
||||||
|
name: 'Name',
|
||||||
|
name_error:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
|
||||||
|
email: 'Email',
|
||||||
|
email_error:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
|
||||||
|
password: 'Password',
|
||||||
|
password_error:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
|
||||||
|
remember: 'Remember me',
|
||||||
|
forgot_password: 'Forgot password?',
|
||||||
|
sign_in_btn: 'sign in',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default locale;
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
import SignOutPage from './SignOutPage';
|
import SignOutPage from './SignOutPage';
|
||||||
|
import en from './i18n/en';
|
||||||
|
|
||||||
|
i18next.addResourceBundle('en', 'signOutPage', en);
|
||||||
|
|
||||||
const SignOutConfig = {
|
const SignOutConfig = {
|
||||||
settings: {
|
settings: {
|
||||||
@@ -7,18 +12,6 @@ const SignOutConfig = {
|
|||||||
navbar: {
|
navbar: {
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
toolbar: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
leftSidePanel: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
rightSidePanel: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import JwtService from '../../auth/services/jwtService';
|
import { authService } from 'src/app/services';
|
||||||
|
|
||||||
function SignOutPage() {
|
function SignOutPage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
JwtService.logout();
|
authService.logout();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
3
src/app/main/authPages/sign-out/i18n/en.js
Normal file
3
src/app/main/authPages/sign-out/i18n/en.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const locale = {};
|
||||||
|
|
||||||
|
export default locale;
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
import SignUpPage from './SignUpPage';
|
import SignUpPage from './SignUpPage';
|
||||||
import authRoles from '../../auth/authRoles';
|
import { authRoles } from '../../../configs/consts';
|
||||||
|
import en from './i18n/en';
|
||||||
|
|
||||||
|
i18next.addResourceBundle('en', 'signUpPage', en);
|
||||||
|
|
||||||
const SignUpConfig = {
|
const SignUpConfig = {
|
||||||
settings: {
|
settings: {
|
||||||
@@ -8,18 +13,6 @@ const SignUpConfig = {
|
|||||||
navbar: {
|
navbar: {
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
toolbar: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
leftSidePanel: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
rightSidePanel: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
187
src/app/main/authPages/sign-up/SignUpPage.js
Normal file
187
src/app/main/authPages/sign-up/SignUpPage.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import _ from '@lodash';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { authService } from 'src/app/services';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import LeftSideCanvas from '../shared-components/LeftSideCanvas';
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
passwordConfirm: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledTextField = forwardRef((props, ref) => (
|
||||||
|
<TextField
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
function SignUpPage({ t }) {
|
||||||
|
const schema = yup.object().shape({
|
||||||
|
name: yup.string().required(t('name_error')),
|
||||||
|
email: yup.string().email(t('email_error')).required(t('email_error')),
|
||||||
|
password: yup.string().required(t('password_error')).min(8, t('password_error')),
|
||||||
|
passwordConfirm: yup.string().oneOf([yup.ref('password'), null], t('password_confirm_error')),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { control, formState, handleSubmit, setError } = useForm({
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues,
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isValid, dirtyFields, errors } = formState;
|
||||||
|
|
||||||
|
function onSubmit({ name, password, email }) {
|
||||||
|
authService
|
||||||
|
.createUser({
|
||||||
|
displayName: name,
|
||||||
|
password,
|
||||||
|
email,
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setError('root', {
|
||||||
|
type: 'manual',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-auto min-w-0">
|
||||||
|
<LeftSideCanvas title={t('title')} subtitle={t('subtitle')} text={t('text')} />
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
className="h-full w-full sm:h-auto md:flex md:h-full py-32 px-16 sm:p-48 md:p-40 md:px-96 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none rtl:border-r-1 ltr:border-l-1"
|
||||||
|
sx={{ background: (theme) => theme.palette.background?.default }}
|
||||||
|
>
|
||||||
|
<div className="w-full mx-auto sm:mx-0">
|
||||||
|
<Typography className="text-4xl font-extrabold tracking-tight leading-tight">
|
||||||
|
{t('sign_up')}
|
||||||
|
</Typography>
|
||||||
|
<div className="flex items-baseline mt-10 font-medium">
|
||||||
|
<Typography>{t('have_account')}</Typography>
|
||||||
|
<Link className="ml-4 text-secondary-main underline" to="/sign-in">
|
||||||
|
{t('sign_in')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
name="signupForm"
|
||||||
|
noValidate
|
||||||
|
className="flex flex-col justify-center w-full mt-48"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
className="mb-28"
|
||||||
|
label={t('name')}
|
||||||
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
error={!!errors.name}
|
||||||
|
helperText={errors?.name?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="email"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
className="mb-28"
|
||||||
|
label={t('email')}
|
||||||
|
type="email"
|
||||||
|
error={!!errors.email}
|
||||||
|
helperText={errors?.email?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="password"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
className="mb-28"
|
||||||
|
label={t('password')}
|
||||||
|
type="password"
|
||||||
|
error={!!errors.password}
|
||||||
|
helperText={errors?.password?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="passwordConfirm"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
className="mb-28"
|
||||||
|
label={t('password_confirm')}
|
||||||
|
type="password"
|
||||||
|
error={!!errors.passwordConfirm}
|
||||||
|
helperText={errors?.passwordConfirm?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center justify-center gap-10 w-full">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
className="max-w-320 mt-32 text-base uppercase rounded-xl"
|
||||||
|
aria-label={t('sign_up_btn')}
|
||||||
|
disabled={_.isEmpty(dirtyFields) || !isValid}
|
||||||
|
type="submit"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{t('sign_up_btn')}
|
||||||
|
</Button>
|
||||||
|
{errors.root?.message && (
|
||||||
|
<p className="text-l text-error-main">{errors.root?.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('signUpPage')(SignUpPage);
|
||||||
24
src/app/main/authPages/sign-up/i18n/en.js
Normal file
24
src/app/main/authPages/sign-up/i18n/en.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const locale = {
|
||||||
|
title: 'Lorem ipsum dolor sit amet!',
|
||||||
|
subtitle:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Scelerisque blandit sit sagittis justo viverra. Morbi accumsaniam elementum enim commodo sed mauris vel. Scelerisque rhoncus in metus non arcu cursus non rhoncus.',
|
||||||
|
text: 'Lorem ipsum dolor sit amet consectetur. Scelerisque blandit sit.',
|
||||||
|
sign_up: 'Sign Up',
|
||||||
|
have_account: 'Already have an account?',
|
||||||
|
sign_in: 'Sign in',
|
||||||
|
name: 'Name',
|
||||||
|
name_error:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
|
||||||
|
email: 'Email',
|
||||||
|
email_error:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
|
||||||
|
password: 'Password',
|
||||||
|
password_error:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
|
||||||
|
password_confirm: 'Password (Confirm)',
|
||||||
|
password_confirm_error:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
|
||||||
|
sign_up_btn: 'create your account',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default locale;
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import FusePageSimple from '@fuse/core/FusePageSimple';
|
|
||||||
import DemoContent from '@fuse/core/DemoContent';
|
|
||||||
|
|
||||||
const Root = styled(FusePageSimple)(({ theme }) => ({
|
|
||||||
'& .FusePageSimple-header': {
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderStyle: 'solid',
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
},
|
|
||||||
'& .FusePageSimple-toolbar': {},
|
|
||||||
'& .FusePageSimple-content': {},
|
|
||||||
'& .FusePageSimple-sidebarHeader': {},
|
|
||||||
'& .FusePageSimple-sidebarContent': {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
function ExamplePage(props) {
|
|
||||||
const { t } = useTranslation('examplePage');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Root
|
|
||||||
header={
|
|
||||||
<div className="p-24">
|
|
||||||
<h4>{t('TITLE')}</h4>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
content={
|
|
||||||
<div className="p-24">
|
|
||||||
<h4>Content</h4>
|
|
||||||
<br />
|
|
||||||
<DemoContent />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
scroll="content"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ExamplePage;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
const locale = {
|
|
||||||
TITLE: 'مثال على الصفحة',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default locale;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
const locale = {
|
|
||||||
TITLE: 'Örnek Sayfa',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default locale;
|
|
||||||
67
src/app/main/home/Home.js
Normal file
67
src/app/main/home/Home.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import AboutUs from './components/AboutUs';
|
||||||
|
import ArticleCardsList from './components/ArticleCardsList';
|
||||||
|
import FeedbackForm from './components/FeedbackForm';
|
||||||
|
import Statistics from './components/Statistics';
|
||||||
|
import Welcome from './components/Welcome';
|
||||||
|
|
||||||
|
const articleCardsMock = [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
title: 'Lorem ipsum dolor sit amet ornare amet consequat ultricies auctor.',
|
||||||
|
description:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Quam molestie non eget rhoncus ut sed. Egestas scelerisque fames lorem id luctus viverra ligula placerat mus. Lorem ipsum dolor sit amet consectetur. Quam molestie non eget rhoncus ut sed. Egestas scelerisque fames lorem id luctus viverra ligula placmeget rhoncus ut settrnhg ips...',
|
||||||
|
image:
|
||||||
|
'https://images.unsplash.com/photo-1664575602276-acd073f104c1?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80',
|
||||||
|
updated: '12.12.2022',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '234',
|
||||||
|
title: 'Lorem ipsum dolor sit amet ornare amet consequat us auctoit amet orare ametr...',
|
||||||
|
description:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Quam molestie non eget rhoncusnon eget rhoncus ut sed. Egestas scelerisque fames lorem id luctus viverra ligula placerat mus.',
|
||||||
|
image:
|
||||||
|
'https://images.unsplash.com/photo-1664575602276-acd073f104c1?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80',
|
||||||
|
updated: '12.12.2022',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '345',
|
||||||
|
title: 'Lorem ipsum dolor sit amet ornare amet consequat ultricies auctor.',
|
||||||
|
description:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Quam molestie non eget rhoncus ut sed. Egestas scelerisque fames lorem id luctus viverra ligula placerat mus. Lorem ipsum dolor sit amet consectetur. Quames lorem id luctus viverra ligula placerat mus.',
|
||||||
|
image:
|
||||||
|
'https://images.unsplash.com/photo-1664575602276-acd073f104c1?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1170&q=80',
|
||||||
|
updated: '12.12.2022',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function Home({ t }) {
|
||||||
|
const [servicesCards, setServicesCards] = useState([]);
|
||||||
|
const [blogCards, setBlogCards] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setServicesCards(articleCardsMock);
|
||||||
|
setBlogCards(articleCardsMock);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full bg-primary-main">
|
||||||
|
<Welcome />
|
||||||
|
<div className="max-w-8xl w-full px-10 mx-auto">
|
||||||
|
<Statistics />
|
||||||
|
<AboutUs />
|
||||||
|
<ArticleCardsList title={t('services_title')} cards={servicesCards} className="mb-20" />
|
||||||
|
<ArticleCardsList
|
||||||
|
title={t('blog_title')}
|
||||||
|
cards={blogCards}
|
||||||
|
id="blog"
|
||||||
|
className="pt-72 mb-28"
|
||||||
|
/>
|
||||||
|
<FeedbackForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('homePage')(Home);
|
||||||
24
src/app/main/home/HomeConfig.js
Normal file
24
src/app/main/home/HomeConfig.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
|
import Home from './Home';
|
||||||
|
import en from './i18n/en';
|
||||||
|
|
||||||
|
i18next.addResourceBundle('en', 'homePage', en);
|
||||||
|
|
||||||
|
const HomeConfig = {
|
||||||
|
settings: {
|
||||||
|
layout: {
|
||||||
|
config: {},
|
||||||
|
style: 'layout2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: null,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <Home />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomeConfig;
|
||||||
37
src/app/main/home/components/AboutUs.js
Normal file
37
src/app/main/home/components/AboutUs.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
function AboutUs({ t }) {
|
||||||
|
return (
|
||||||
|
<section id="about-us" className="flex flex-col items-center pt-72 mb-80">
|
||||||
|
<h2 className="self-start mb-56 text-[48px] font-semibold">{t('about_us_title')}</h2>
|
||||||
|
<div className="flex gap-64 mb-[126px]">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<iframe
|
||||||
|
className="rounded-20"
|
||||||
|
width="715"
|
||||||
|
height="402"
|
||||||
|
src="https://www.youtube.com/embed/rNSIwjmynYQ?controls=0"
|
||||||
|
title="YouTube video player"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<aside className="flex flex-col items-center py-40 px-52 bg-primary-light rounded-20">
|
||||||
|
<h3 className="mb-16 text-lg text-common-layout font-medium">{t('about_us_subject')}</h3>
|
||||||
|
<p className="mb-16 text-lg text-common-layout font-light">{t('about_us_text_1')}</p>
|
||||||
|
<p className="mb-16 text-lg text-common-layout font-light">{t('about_us_text_2')}</p>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
className="w-[220px] py-[17px] text-center text-base text-primary-light font-semibold tracking-widest uppercase rounded-2xl bg-secondary-light shadow hover:shadow-hover hover:shadow-secondary-light ease-in-out duration-300"
|
||||||
|
to="/rent-and-buy/search"
|
||||||
|
>
|
||||||
|
{t('research_btn')}
|
||||||
|
</Link>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('homePage')(memo(AboutUs));
|
||||||
33
src/app/main/home/components/ArticleCard.js
Normal file
33
src/app/main/home/components/ArticleCard.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
function ArticleCard({ t, id, title, description, image, updated }) {
|
||||||
|
return (
|
||||||
|
<article className="flex flex-col justify-between max-w-[460px] w-full h-[526px] bg-primary-light rounded-20 shadow-light">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
className="w-full h-[230px] mb-20 rounded-20 object-cover"
|
||||||
|
src={image}
|
||||||
|
alt={title}
|
||||||
|
width="460"
|
||||||
|
height="230"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<h3 className="px-[15px] mb-20 text-xl leading-light font-medium">{title}</h3>
|
||||||
|
<p className="px-[15px] text-lg text-common-layout font-light">{description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="px-[15px] mb-[17px] flex justify-between items-center">
|
||||||
|
<p className="text-lg text-common-secondary font-light">{updated}</p>
|
||||||
|
<Link
|
||||||
|
className="flex items-center justify-center w-[60px] h-[60px] text-secondary-main rounded-full border-2 border-secondary-main"
|
||||||
|
to={`/blog/${id}`}
|
||||||
|
>
|
||||||
|
{t('article_btn')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('homePage')(memo(ArticleCard));
|
||||||
17
src/app/main/home/components/ArticleCardsList.js
Normal file
17
src/app/main/home/components/ArticleCardsList.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import ArticleCard from './ArticleCard';
|
||||||
|
|
||||||
|
function ArticleCardsList({ title, cards, ...attrs }) {
|
||||||
|
return (
|
||||||
|
<section {...attrs}>
|
||||||
|
<h2 className="mb-56 text-[48px] font-semibold">{title}</h2>
|
||||||
|
<div className="flex gap-20">
|
||||||
|
{cards.map((card) => (
|
||||||
|
<ArticleCard {...card} key={card.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ArticleCardsList);
|
||||||
157
src/app/main/home/components/FeedbackForm.js
Normal file
157
src/app/main/home/components/FeedbackForm.js
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import _ from '@lodash';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import { forwardRef, memo, useState } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledTextField = forwardRef((props, ref) => (
|
||||||
|
<TextField
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
function FeedbackForm({ t }) {
|
||||||
|
const [isFormSubmitted, setIsFormSubmitted] = useState(false);
|
||||||
|
|
||||||
|
const schema = yup.object().shape({
|
||||||
|
name: yup.string().required(t('feedback_required')),
|
||||||
|
email: yup.string().email(t('feedback_email_error')).required(t('feedback_required')),
|
||||||
|
message: yup
|
||||||
|
.string()
|
||||||
|
.required(t('feedback_required'))
|
||||||
|
.min(5, t('min_length_error', { length: 5 }))
|
||||||
|
.max(255, t('max_length_error', { length: 255 })),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { control, formState, handleSubmit, setError } = useForm({
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues,
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isValid, dirtyFields, errors } = formState;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
// setError('root', 'error');
|
||||||
|
setIsFormSubmitted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="contacts" className="pt-72 mb-88">
|
||||||
|
{!isFormSubmitted && (
|
||||||
|
<form
|
||||||
|
name="signinForm"
|
||||||
|
noValidate
|
||||||
|
className="grid grid-cols-2 gap-x-20 gap-y-32 px-80 py-40 bg-primary-light rounded-20"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<legend className="col-span-2 justify-self-center max-w-[860px] mb-8 text-4xl font-medium text-center">
|
||||||
|
{t('feedback_title')}
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
label={t('feedback_name')}
|
||||||
|
type="text"
|
||||||
|
error={!!errors.name}
|
||||||
|
helperText={errors?.name?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="email"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
label={t('feedback_email')}
|
||||||
|
type="email"
|
||||||
|
error={!!errors.email}
|
||||||
|
helperText={errors?.email?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="message"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-2 mb-8"
|
||||||
|
label={t('feedback_message')}
|
||||||
|
type="text"
|
||||||
|
error={!!errors.message}
|
||||||
|
helperText={errors?.message?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
multiline
|
||||||
|
rows={5}
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
padding: 0,
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
// eslint-disable-next-line react/jsx-no-duplicate-props
|
||||||
|
inputProps={{
|
||||||
|
sx: {
|
||||||
|
padding: '16.5px 14px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="col-span-2 justify-self-center flex flex-col items-center justify-center gap-10 w-full">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
className="w-[220px] text-base uppercase rounded-xl"
|
||||||
|
aria-label={t('feedback_btn')}
|
||||||
|
disabled={_.isEmpty(dirtyFields) || !isValid}
|
||||||
|
type="submit"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{t('feedback_btn')}
|
||||||
|
</Button>
|
||||||
|
{errors.root?.message && (
|
||||||
|
<p className="text-l text-error-main">{errors.root?.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('homePage')(memo(FeedbackForm));
|
||||||
18
src/app/main/home/components/Statistics.js
Normal file
18
src/app/main/home/components/Statistics.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import StatisticsCard from './StatisticsCard';
|
||||||
|
|
||||||
|
function Statistics({ t }) {
|
||||||
|
return (
|
||||||
|
<section className="flex flex-col items-center">
|
||||||
|
<div className="flex gap-20 mb-[136px]">
|
||||||
|
<StatisticsCard title={t('stat_title_1')} text={t('stat_text_1')} />
|
||||||
|
<StatisticsCard title={t('stat_title_2')} text={t('stat_text_2')} />
|
||||||
|
<StatisticsCard title={t('stat_title_3')} text={t('stat_text_3')} />
|
||||||
|
</div>
|
||||||
|
<h2 className="max-w-[1020px] mb-4 text-5xl text-center">{t('caption')}</h2>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('homePage')(memo(Statistics));
|
||||||
12
src/app/main/home/components/StatisticsCard.js
Normal file
12
src/app/main/home/components/StatisticsCard.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
function StatisticsCard({ title, text }) {
|
||||||
|
return (
|
||||||
|
<article className="flex flex-col justify-start items-center max-w-[460px] w-full min-h-[356px] h-full pt-32 px-40 text-common-primary bg-primary-light rounded-20 shadow-light even:bg-secondary-main even:text-primary-light">
|
||||||
|
<h3 className="mb-52 text-[80px] font-semibold">{title}</h3>
|
||||||
|
<p className="text-xl leading-5 font-light">{text}</p>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(StatisticsCard);
|
||||||
48
src/app/main/home/components/Welcome.js
Normal file
48
src/app/main/home/components/Welcome.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import SearchInput from '../../shared-components/SearchInput';
|
||||||
|
|
||||||
|
function Welcome({ t }) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const onInputType = (event) => {
|
||||||
|
const { target } = event;
|
||||||
|
const value = target?.value ?? '';
|
||||||
|
setQuery(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearch = () => {
|
||||||
|
const trimmedQuery = query.trim();
|
||||||
|
if (!trimmedQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(`/rent-and-buy/search?query=${trimmedQuery}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="h-[calc(100vh-72px)] mb-[105px] bg-home-welcome bg-cover">
|
||||||
|
<div className="flex flex-col max-w-8xl w-full mx-auto pt-160 px-10">
|
||||||
|
<h1 className="max-w-[910px] mb-[30px] text-7xl font-bold text-primary-light">
|
||||||
|
{t('title')}
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-[820px] mb-[100px] text-2xl font-medium text-primary-light">
|
||||||
|
{t('subtitle')}
|
||||||
|
</p>
|
||||||
|
<SearchInput
|
||||||
|
className="max-w-[780px]"
|
||||||
|
mode="simple"
|
||||||
|
placeholder={t('main_input_placeholder')}
|
||||||
|
btnText={t('main_input_btn')}
|
||||||
|
query={query}
|
||||||
|
onType={onInputType}
|
||||||
|
onSearch={onSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('homePage')(memo(Welcome));
|
||||||
41
src/app/main/home/i18n/en.js
Normal file
41
src/app/main/home/i18n/en.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const locale = {
|
||||||
|
title: 'Lorem ipsum dolor sit amet ornare amet consequat ultricies auctor.',
|
||||||
|
subtitle:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Quam molestie non eget rhoncus ut sed. Egestas scelerisque fames lorem id luctus viverra ligula placerat mus.',
|
||||||
|
main_input_placeholder: 'Lorem ipsum dolor sit amemolestie non eget rhoncus ut sed.',
|
||||||
|
main_input_btn: 'calculate',
|
||||||
|
stat_title_1: '158',
|
||||||
|
stat_text_1:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Quam molestie non eget rhoncus ut sed. Egestas scelerisque fames lorem id luctus viverra ligula placerat mus.',
|
||||||
|
stat_title_2: '89%',
|
||||||
|
stat_text_2:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Quam molestie non eget rhoncus ut sed. Egestas scelerisque fames lom id luctus viverra ligula placerat mus.',
|
||||||
|
stat_title_3: '10k+',
|
||||||
|
stat_text_3:
|
||||||
|
'Egestas scelerisque fames lorem. Lorem ipsum dolor sit amet consectetur. Quam molestie non eget rhoncus ut sed. Egestas scelerisque fames lorem id luctus viverra ligula placerat mus.',
|
||||||
|
caption:
|
||||||
|
'Lorem ipsum dolor sit amet ornare amet consequat ultricies sit amet ornare amet consequat ultricie auctor.',
|
||||||
|
about_us_title: 'About us',
|
||||||
|
about_us_watch: 'Watch Demo',
|
||||||
|
about_us_subject:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eu amet tellus tristique viverra accumsan ac vel eu. Ut morbi tempor quam elit orci nulla mattis. Vitae vitae egestas amet at gravida montes sit. Eu sit at sapien enim platea eget arcu. Sed consectetur sit in aliquam mi tellus at scelerisque. Tempor lacus sit ut augue amet penatibus amet malesuada orci.',
|
||||||
|
about_us_text_1:
|
||||||
|
'Lorevinar adipiscing tempus interdum lobortis. Mauris porta sagittis sed tempor tra urna. Volutpat dui nisl lorem gravida enim ut habitant sit. Natoque viverra habitasse tincidunt tristique sit.',
|
||||||
|
about_us_text_2:
|
||||||
|
'Vel phasellus pellentesque duis lorem maecenas. Vestibulum dui massa elit suspendisse porttitor integer praesent. Aliquam massa ante vestibulum neque sed imperdiet rhoncus. Turpis quam sed nibh id ultricies. Mattis mattis sit enim nunc interdum adipiscing. Arcu maecenas quis eget eget nunc quam. Id et quis enim morbi magnis. Nam ut habitasse sagittis magna morbi augue at.',
|
||||||
|
research_btn: 'research',
|
||||||
|
services_title: 'Services',
|
||||||
|
blog_title: 'Blog',
|
||||||
|
article_btn: 'See',
|
||||||
|
feedback_title: 'Lorem ipsum dolor sit amet ornare amet consequat us auctoit amet orare ametr...',
|
||||||
|
feedback_name: 'Name',
|
||||||
|
feedback_email: 'Email',
|
||||||
|
feedback_email_error: 'incorrect email',
|
||||||
|
feedback_message: 'Message',
|
||||||
|
feedback_btn: 'send',
|
||||||
|
feedback_required: 'this field is required',
|
||||||
|
max_length_error: 'The maximum length is {{length}}',
|
||||||
|
min_length_error: 'The minimum length is {{length}}',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default locale;
|
||||||
95
src/app/main/navigationPages/dashboard/Dashboard.js
Normal file
95
src/app/main/navigationPages/dashboard/Dashboard.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import FusePageSimple from '@fuse/core/FusePageSimple';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import SearchInput from '../../shared-components/SearchInput';
|
||||||
|
import DashboardCategory from '../shared-components/DashboardCategory';
|
||||||
|
|
||||||
|
const categoriesMock = [
|
||||||
|
{
|
||||||
|
title: 'All Properties',
|
||||||
|
value: 34,
|
||||||
|
valueColor: 'secondary-main',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'New',
|
||||||
|
value: 12,
|
||||||
|
valueColor: 'common-highlight2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'In Research',
|
||||||
|
value: 3,
|
||||||
|
valueColor: 'secondary-main',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Interested',
|
||||||
|
value: 25,
|
||||||
|
valueColor: 'common-highlight1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Purchased',
|
||||||
|
value: 8,
|
||||||
|
valueColor: 'accept-main',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Not Interested',
|
||||||
|
value: 11,
|
||||||
|
valueColor: 'error-main',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Root = styled(FusePageSimple)(({ theme }) => ({
|
||||||
|
'& .FusePageSimple-header': {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
},
|
||||||
|
'& .FusePageSimple-toolbar': {},
|
||||||
|
'& .FusePageSimple-content': {},
|
||||||
|
'& .FusePageSimple-sidebarHeader': {},
|
||||||
|
'& .FusePageSimple-sidebarContent': {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function DashboardPage({ t }) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
|
const onInputType = (event) => {
|
||||||
|
const { target } = event;
|
||||||
|
const value = target?.value ?? '';
|
||||||
|
setQuery(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearch = () => {
|
||||||
|
// query
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root
|
||||||
|
content={
|
||||||
|
<div className="w-full p-60">
|
||||||
|
<div className="flex flex-wrap justify-center items-center gap-20 mb-52">
|
||||||
|
{categoriesMock.map(({ title, value, valueColor }) => (
|
||||||
|
<DashboardCategory title={title} value={value} valueColor={valueColor} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchInput
|
||||||
|
className="mb-28"
|
||||||
|
mode="manual"
|
||||||
|
btnText={t('search_input_btn')}
|
||||||
|
query={query}
|
||||||
|
onType={onInputType}
|
||||||
|
onSearch={onSearch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Paper className="w-full h-640 mb-[30px] rounded-20 shadow-light" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
scroll="content"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('dashboardPage')(DashboardPage);
|
||||||
@@ -1,29 +1,27 @@
|
|||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
|
|
||||||
|
import { authRoles } from '../../../configs/consts';
|
||||||
|
import Dashboard from './Dashboard';
|
||||||
import en from './i18n/en';
|
import en from './i18n/en';
|
||||||
import tr from './i18n/tr';
|
|
||||||
import ar from './i18n/ar';
|
|
||||||
import Example from './Example';
|
|
||||||
|
|
||||||
i18next.addResourceBundle('en', 'examplePage', en);
|
i18next.addResourceBundle('en', 'dashboardPage', en);
|
||||||
i18next.addResourceBundle('tr', 'examplePage', tr);
|
|
||||||
i18next.addResourceBundle('ar', 'examplePage', ar);
|
|
||||||
|
|
||||||
const ExampleConfig = {
|
const DashboardConfig = {
|
||||||
settings: {
|
settings: {
|
||||||
layout: {
|
layout: {
|
||||||
config: {},
|
config: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
auth: authRoles.user,
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: 'example',
|
path: 'dashboard',
|
||||||
element: <Example />,
|
element: <Dashboard />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExampleConfig;
|
export default DashboardConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazy load Example
|
* Lazy load Example
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
const locale = {
|
const locale = {
|
||||||
TITLE: 'Example Page',
|
search_input_btn: 'calculate',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default locale;
|
export default locale;
|
||||||
101
src/app/main/navigationPages/favorites/Favorites.js
Normal file
101
src/app/main/navigationPages/favorites/Favorites.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import FusePageSimple from '@fuse/core/FusePageSimple';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { PROPERTIES_LAYOUTS } from 'app/configs/consts';
|
||||||
|
import { selectUserFavorites, updateUserFavorites } from 'app/store/userSlice';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { usePropertiesHeader } from 'src/app/hooks';
|
||||||
|
import PropertiesHeader from '../shared-components/PropertiesHeader';
|
||||||
|
import PropertyGridCard from '../shared-components/PropertyGridCard';
|
||||||
|
import PropertyListItem from '../shared-components/PropertyListItem';
|
||||||
|
|
||||||
|
const Root = styled(FusePageSimple)(({ theme }) => ({
|
||||||
|
'& .FusePageSimple-header': {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
},
|
||||||
|
'& .FusePageSimple-toolbar': {},
|
||||||
|
'& .FusePageSimple-content': {},
|
||||||
|
'& .FusePageSimple-sidebarHeader': {},
|
||||||
|
'& .FusePageSimple-sidebarContent': {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function FavoritesPage() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const items = useSelector(selectUserFavorites);
|
||||||
|
|
||||||
|
const { categories, activeCategory, onCategory, layouts, activeLayout, onLayout, onItemDelete } =
|
||||||
|
usePropertiesHeader(items);
|
||||||
|
|
||||||
|
const onFavorite = useCallback(
|
||||||
|
(id) => {
|
||||||
|
const targetItem = items.find((item) => item.id === id);
|
||||||
|
if (!targetItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onItemDelete(targetItem?.category);
|
||||||
|
dispatch(updateUserFavorites(targetItem)).catch((error) => console.log(error));
|
||||||
|
},
|
||||||
|
[items, onItemDelete, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderedItems = useMemo(
|
||||||
|
() =>
|
||||||
|
items.map((item, idx) => {
|
||||||
|
if (activeCategory !== 'all' && item.category !== activeCategory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
return activeLayout === PROPERTIES_LAYOUTS.list ? (
|
||||||
|
<PropertyListItem
|
||||||
|
{...item}
|
||||||
|
key={item.title + idx}
|
||||||
|
onDelete={onFavorite}
|
||||||
|
onFavorite={onFavorite}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PropertyGridCard
|
||||||
|
{...item}
|
||||||
|
key={item.title + idx}
|
||||||
|
onDelete={onFavorite}
|
||||||
|
onFavorite={onFavorite}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[items, activeCategory, activeLayout, onFavorite]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root
|
||||||
|
content={
|
||||||
|
<div className="w-full p-60">
|
||||||
|
<Paper className="w-full h-320 mb-[30px] rounded-20 shadow-light" />
|
||||||
|
<PropertiesHeader
|
||||||
|
className="mb-40"
|
||||||
|
categories={categories}
|
||||||
|
layouts={layouts}
|
||||||
|
onCategory={onCategory}
|
||||||
|
onLayout={onLayout}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'w-full flex flex-wrap justify-center gap-28',
|
||||||
|
activeLayout === PROPERTIES_LAYOUTS.list && 'flex-col'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderedItems}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
scroll="content"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FavoritesPage;
|
||||||
26
src/app/main/navigationPages/favorites/FavoritesConfig.js
Normal file
26
src/app/main/navigationPages/favorites/FavoritesConfig.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { lazy } from 'react';
|
||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
|
import { authRoles } from '../../../configs/consts';
|
||||||
|
import en from './i18n/en';
|
||||||
|
|
||||||
|
i18next.addResourceBundle('en', 'favoritesPage', en);
|
||||||
|
|
||||||
|
const Favorites = lazy(() => import('./Favorites'));
|
||||||
|
|
||||||
|
const FavoritesConfig = {
|
||||||
|
settings: {
|
||||||
|
layout: {
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: authRoles.user,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: 'favorites',
|
||||||
|
element: <Favorites />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FavoritesConfig;
|
||||||
3
src/app/main/navigationPages/favorites/i18n/en.js
Normal file
3
src/app/main/navigationPages/favorites/i18n/en.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const locale = {};
|
||||||
|
|
||||||
|
export default locale;
|
||||||
111
src/app/main/navigationPages/history/History.js
Normal file
111
src/app/main/navigationPages/history/History.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import FusePageSimple from '@fuse/core/FusePageSimple';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { PROPERTIES_LAYOUTS } from 'app/configs/consts';
|
||||||
|
import { selectUserHistory, updateUserFavorites, updateUserHistory } from 'app/store/userSlice';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { usePropertiesHeader } from 'src/app/hooks';
|
||||||
|
import PropertiesHeader from '../shared-components/PropertiesHeader';
|
||||||
|
import PropertyGridCard from '../shared-components/PropertyGridCard';
|
||||||
|
import PropertyListItem from '../shared-components/PropertyListItem';
|
||||||
|
|
||||||
|
const Root = styled(FusePageSimple)(({ theme }) => ({
|
||||||
|
'& .FusePageSimple-header': {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
},
|
||||||
|
'& .FusePageSimple-toolbar': {},
|
||||||
|
'& .FusePageSimple-content': {},
|
||||||
|
'& .FusePageSimple-sidebarHeader': {},
|
||||||
|
'& .FusePageSimple-sidebarContent': {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
function HistoryPage() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const items = useSelector(selectUserHistory);
|
||||||
|
|
||||||
|
const { categories, activeCategory, onCategory, layouts, activeLayout, onLayout, onItemDelete } =
|
||||||
|
usePropertiesHeader(items);
|
||||||
|
|
||||||
|
const onDelete = useCallback(
|
||||||
|
(id) => {
|
||||||
|
const targetItem = items.find((item) => item.id === id);
|
||||||
|
const newHistory = items.filter((item) => item.id !== id);
|
||||||
|
|
||||||
|
onItemDelete(targetItem?.category);
|
||||||
|
dispatch(updateUserHistory(newHistory));
|
||||||
|
},
|
||||||
|
[items, onItemDelete, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFavorite = useCallback(
|
||||||
|
(id) => {
|
||||||
|
const targetItem = items.find((item) => item.id === id);
|
||||||
|
if (!targetItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(updateUserFavorites(targetItem)).catch((error) => console.log(error));
|
||||||
|
},
|
||||||
|
[items, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderedItems = useMemo(
|
||||||
|
() =>
|
||||||
|
items.map((item, idx) => {
|
||||||
|
if (activeCategory !== 'all' && item.category !== activeCategory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
return activeLayout === PROPERTIES_LAYOUTS.list ? (
|
||||||
|
<PropertyListItem
|
||||||
|
{...item}
|
||||||
|
key={item.title + idx}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onFavorite={onFavorite}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PropertyGridCard
|
||||||
|
{...item}
|
||||||
|
key={item.title + idx}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onFavorite={onFavorite}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[items, activeCategory, activeLayout, onDelete, onFavorite]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root
|
||||||
|
content={
|
||||||
|
<div className="w-full p-60">
|
||||||
|
<Paper className="w-full h-320 mb-[30px] rounded-20 shadow-light" />
|
||||||
|
<PropertiesHeader
|
||||||
|
className="mb-40"
|
||||||
|
categories={categories}
|
||||||
|
layouts={layouts}
|
||||||
|
onCategory={onCategory}
|
||||||
|
onLayout={onLayout}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'w-full flex flex-wrap justify-center gap-28',
|
||||||
|
activeLayout === PROPERTIES_LAYOUTS.list && 'flex-col'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderedItems}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
scroll="content"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryPage;
|
||||||
26
src/app/main/navigationPages/history/HistoryConfig.js
Normal file
26
src/app/main/navigationPages/history/HistoryConfig.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { lazy } from 'react';
|
||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
|
import { authRoles } from '../../../configs/consts';
|
||||||
|
import en from './i18n/en';
|
||||||
|
|
||||||
|
i18next.addResourceBundle('en', 'historyPage', en);
|
||||||
|
|
||||||
|
const History = lazy(() => import('./History'));
|
||||||
|
|
||||||
|
const HistoryConfig = {
|
||||||
|
settings: {
|
||||||
|
layout: {
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: authRoles.user,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: 'history',
|
||||||
|
element: <History />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoryConfig;
|
||||||
3
src/app/main/navigationPages/history/i18n/en.js
Normal file
3
src/app/main/navigationPages/history/i18n/en.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const locale = {};
|
||||||
|
|
||||||
|
export default locale;
|
||||||
8
src/app/main/navigationPages/navigationPagesConfig.js
Normal file
8
src/app/main/navigationPages/navigationPagesConfig.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import DashboardConfig from './dashboard/DashboardConfig';
|
||||||
|
import FavoritesConfig from './favorites/FavoritesConfig';
|
||||||
|
import HistoryConfig from './history/HistoryConfig';
|
||||||
|
import ProfileConfig from './profile/ProfileConfig';
|
||||||
|
|
||||||
|
const navigationPagesConfigs = [DashboardConfig, FavoritesConfig, HistoryConfig, ProfileConfig];
|
||||||
|
|
||||||
|
export default navigationPagesConfigs;
|
||||||
383
src/app/main/navigationPages/profile/Profile.js
Normal file
383
src/app/main/navigationPages/profile/Profile.js
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
import FusePageSimple from '@fuse/core/FusePageSimple';
|
||||||
|
import _ from '@lodash';
|
||||||
|
import FuseSvgIcon from '@fuse/core/FuseSvgIcon/FuseSvgIcon';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { selectUser, updateUserSettings } from 'app/store/userSlice';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import { toBase64 } from 'src/app/utils';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
const MAX_PICTURE_SIZE = 5000000;
|
||||||
|
const AVAILABLE_MEDIA_TYPES = ['image/png', 'image/jpeg'];
|
||||||
|
|
||||||
|
const Root = styled(FusePageSimple)(({ theme }) => ({
|
||||||
|
'& .FusePageSimple-header': {},
|
||||||
|
'& .FusePageSimple-toolbar': {},
|
||||||
|
'& .FusePageSimple-content': {
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
},
|
||||||
|
'& .FusePageSimple-sidebarHeader': {},
|
||||||
|
'& .FusePageSimple-sidebarContent': {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledTextField = forwardRef((props, ref) => (
|
||||||
|
<TextField
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
function ProfilePage({ t }) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const user = useSelector(selectUser);
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
photoURL: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
displayName: '',
|
||||||
|
email: '',
|
||||||
|
mobileNumber: '',
|
||||||
|
information: '',
|
||||||
|
address: '',
|
||||||
|
...user.data,
|
||||||
|
};
|
||||||
|
const schema = yup.object().shape({
|
||||||
|
photoURL: yup.string().notRequired(),
|
||||||
|
firstName: yup
|
||||||
|
.string()
|
||||||
|
.max(150, t('max_length_error', { length: 150 }))
|
||||||
|
.trim()
|
||||||
|
.notRequired(),
|
||||||
|
lastName: yup
|
||||||
|
.string()
|
||||||
|
.max(150, t('max_length_error', { length: 150 }))
|
||||||
|
.trim()
|
||||||
|
.notRequired(),
|
||||||
|
displayName: yup
|
||||||
|
.string()
|
||||||
|
.required(t('display_name_error'))
|
||||||
|
.max(30, t('max_length_error', { length: 30 }))
|
||||||
|
.trim(),
|
||||||
|
email: yup
|
||||||
|
.string()
|
||||||
|
.matches(/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/g, t('email_error'))
|
||||||
|
.required(t('email_error'))
|
||||||
|
.max(40, t('max_length_error', { length: 40 })),
|
||||||
|
mobileNumber: yup
|
||||||
|
.string()
|
||||||
|
.matches(/^(?=(.*\d){7,})\+?(\d[\d-. ]+)?(\([\d-. ]+\))?[\d-. ]+\d$/, {
|
||||||
|
message: t('mobile_number_error'),
|
||||||
|
excludeEmptyString: true,
|
||||||
|
})
|
||||||
|
.max(20, t('max_length_error', { length: 20 }))
|
||||||
|
.notRequired(),
|
||||||
|
information: yup
|
||||||
|
.string()
|
||||||
|
.max(300, t('max_length_error', { length: 300 }))
|
||||||
|
.trim()
|
||||||
|
.notRequired(),
|
||||||
|
address: yup
|
||||||
|
.string()
|
||||||
|
.max(150, t('max_length_error', { length: 150 }))
|
||||||
|
.trim()
|
||||||
|
.notRequired(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { control, formState, watch, setValue, handleSubmit, setError, reset } = useForm({
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues,
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
});
|
||||||
|
const { dirtyFields, errors } = formState;
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
const uploadPicture = async (event) => {
|
||||||
|
const { target } = event;
|
||||||
|
if (target.files && target.files[0]) {
|
||||||
|
const file = target.files[0];
|
||||||
|
if (file.size > MAX_PICTURE_SIZE) {
|
||||||
|
return setError('photoURL', { type: 'custom', message: t('picture_size_error') });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AVAILABLE_MEDIA_TYPES.includes(file.type)) {
|
||||||
|
return setError('photoURL', {
|
||||||
|
type: 'custom',
|
||||||
|
message: t('picture_extensions_error'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const base64 = await toBase64(file);
|
||||||
|
setValue('photoURL', base64, { shouldDirty: true });
|
||||||
|
} else {
|
||||||
|
setError('photoURL', { type: 'custom', message: 'Choose a file please' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePicture = () => setValue('photoURL', '', { shouldDirty: true });
|
||||||
|
|
||||||
|
const onSubmit = (data) => {
|
||||||
|
dispatch(updateUserSettings(data)).catch((error) => {
|
||||||
|
setError('root', {
|
||||||
|
type: 'manual',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
reset(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root
|
||||||
|
content={
|
||||||
|
<div className="w-full p-60">
|
||||||
|
<form name="profileForm" noValidate onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="flex items-end mb-68">
|
||||||
|
<Paper className="relative w-[240px] h-[240px] mr-20 bg-common-disabled overflow-hidden">
|
||||||
|
{watch('photoURL') && (
|
||||||
|
<img
|
||||||
|
src={watch('photoURL')}
|
||||||
|
alt="user"
|
||||||
|
className="absolute w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
<div className="grid grid-cols-[200px_minmax(0,_1fr)] gap-x-40 gap-y-28">
|
||||||
|
<Controller
|
||||||
|
name="photoURL"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { name, ref, onBlur } }) => (
|
||||||
|
<label htmlFor="input-file">
|
||||||
|
<input
|
||||||
|
id="input-file"
|
||||||
|
name={name}
|
||||||
|
ref={ref}
|
||||||
|
type="file"
|
||||||
|
accept={AVAILABLE_MEDIA_TYPES.join(', ')}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={uploadPicture}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
component="span"
|
||||||
|
color="secondary"
|
||||||
|
className="text-lg rounded-xl border-2 border-secondary-main shadow hover:shadow-hover hover:shadow-secondary-main hover:border-2 ease-in-out duration-300"
|
||||||
|
aria-label={t('upload_picture_btn')}
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{t('upload_picture_btn')}
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
color="secondary"
|
||||||
|
className="justify-self-start min-w-fit text-base"
|
||||||
|
aria-label={t('delete_picture')}
|
||||||
|
disabled={!watch('photoURL')}
|
||||||
|
size="small"
|
||||||
|
onClick={deletePicture}
|
||||||
|
>
|
||||||
|
<FuseSvgIcon className="mr-10">heroicons-outline:trash</FuseSvgIcon>
|
||||||
|
{t('delete_picture')}
|
||||||
|
</Button>
|
||||||
|
<ul className="col-span-2 text-lg">
|
||||||
|
<li className="bullet">{t('first_picture_req')}</li>
|
||||||
|
<li className="bullet">{t('second_picture_req')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-6 gap-x-24 gap-y-32 w-full mb-48">
|
||||||
|
<Controller
|
||||||
|
name="firstName"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-2"
|
||||||
|
label={t('first_name')}
|
||||||
|
type="text"
|
||||||
|
error={!!errors.firstName}
|
||||||
|
helperText={errors?.firstName?.message}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="lastName"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-2"
|
||||||
|
label={t('last_name')}
|
||||||
|
type="text"
|
||||||
|
error={!!errors.lastName}
|
||||||
|
helperText={errors?.lastName?.message}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="displayName"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-2"
|
||||||
|
label={t('display_name')}
|
||||||
|
type="text"
|
||||||
|
error={!!errors.displayName}
|
||||||
|
helperText={errors?.displayName?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="email"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-3"
|
||||||
|
label={t('email')}
|
||||||
|
type="email"
|
||||||
|
error={!!errors.email}
|
||||||
|
helperText={errors?.email?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="mobileNumber"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<StyledTextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-3"
|
||||||
|
label={t('mobile_number')}
|
||||||
|
type="tel"
|
||||||
|
error={!!errors.mobileNumber}
|
||||||
|
helperText={errors?.mobileNumber?.message}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="information"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-3"
|
||||||
|
label={t('information')}
|
||||||
|
type="text"
|
||||||
|
error={!!errors.information}
|
||||||
|
helperText={errors?.information?.message}
|
||||||
|
variant="outlined"
|
||||||
|
multiline
|
||||||
|
rows={7}
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
padding: 0,
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
// eslint-disable-next-line react/jsx-no-duplicate-props
|
||||||
|
inputProps={{
|
||||||
|
sx: {
|
||||||
|
padding: '16.5px 14px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="address"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-3"
|
||||||
|
label={t('address')}
|
||||||
|
type="text"
|
||||||
|
error={!!errors.address}
|
||||||
|
helperText={errors?.address?.message}
|
||||||
|
variant="outlined"
|
||||||
|
multiline
|
||||||
|
rows={7}
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
padding: 0,
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
// eslint-disable-next-line react/jsx-no-duplicate-props
|
||||||
|
inputProps={{
|
||||||
|
sx: {
|
||||||
|
padding: '16.5px 14px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center justify-center gap-10 w-full">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
className="max-w-320 text-base uppercase rounded-xl"
|
||||||
|
aria-label={t('save_changes')}
|
||||||
|
disabled={_.isEmpty(dirtyFields) || !_.isEmpty(errors)}
|
||||||
|
type="submit"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{t('save_changes')}
|
||||||
|
</Button>
|
||||||
|
{errors.root?.message && (
|
||||||
|
<p className="text-l text-error-main">{errors.root?.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
scroll="content"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('profilePage')(ProfilePage);
|
||||||
26
src/app/main/navigationPages/profile/ProfileConfig.js
Normal file
26
src/app/main/navigationPages/profile/ProfileConfig.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { lazy } from 'react';
|
||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
|
import { authRoles } from '../../../configs/consts';
|
||||||
|
import en from './i18n/en';
|
||||||
|
|
||||||
|
i18next.addResourceBundle('en', 'profilePage', en);
|
||||||
|
|
||||||
|
const Profile = lazy(() => import('./Profile'));
|
||||||
|
|
||||||
|
const ProfileConfig = {
|
||||||
|
settings: {
|
||||||
|
layout: {
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: authRoles.user,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: 'profile',
|
||||||
|
element: <Profile />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfileConfig;
|
||||||
25
src/app/main/navigationPages/profile/i18n/en.js
Normal file
25
src/app/main/navigationPages/profile/i18n/en.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
const locale = {
|
||||||
|
upload_picture_btn: 'Upload New Picture',
|
||||||
|
delete_picture: 'Delete',
|
||||||
|
first_picture_req: 'Lorem ipsum dolor st ut nec.',
|
||||||
|
second_picture_req: 'Lorem ipsum dolor sit amet consectetur. Etiam tristique feugiat ut nec.',
|
||||||
|
picture_size_error: 'The file is too large',
|
||||||
|
picture_extensions_error: 'We only support jpeg and png file extensions',
|
||||||
|
first_name: 'First Name',
|
||||||
|
last_name: 'Last Name',
|
||||||
|
display_name: 'Display Name',
|
||||||
|
display_name_error:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
|
||||||
|
email: 'Email',
|
||||||
|
email_error:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
|
||||||
|
mobile_number: 'Mobile Number',
|
||||||
|
mobile_number_error: 'The mobile number is not correct',
|
||||||
|
information: 'Biographical Information',
|
||||||
|
address: 'Address',
|
||||||
|
save_changes: 'save changes',
|
||||||
|
max_length_error: 'The maximum length is {{length}}',
|
||||||
|
min_length_error: 'The minimum length is {{length}}',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default locale;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
function DashboardCategory({ className, title, value, valueColor }) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={clsx(
|
||||||
|
'flex flex-col items-center justify-center gap-24 max-w-224 w-full h-160 p-24 bg-white shadow-light rounded-20',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" className="text-lg text-common-layout font-medium">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
className={clsx('text-5xl font-bold', valueColor && `text-${valueColor}`)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Typography>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(DashboardCategory);
|
||||||
17
src/app/main/navigationPages/shared-components/DateMark.js
Normal file
17
src/app/main/navigationPages/shared-components/DateMark.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
function DateMark({ className, update }) {
|
||||||
|
return (
|
||||||
|
<span className={clsx('flex justify-center items-center gap-10', className)}>
|
||||||
|
<FuseSvgIcon>heroicons-outline:calendar</FuseSvgIcon>
|
||||||
|
<Typography variant="body1" className="text-lg font-medium text-common-secondary">
|
||||||
|
{update}
|
||||||
|
</Typography>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(DateMark);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
function UpdateMark({ className, favorite, id, onClick }) {
|
||||||
|
const hasCallback = typeof onClick !== 'undefined';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="w-[24px] h-[24px] cursor-pointer"
|
||||||
|
type="button"
|
||||||
|
onClick={() => hasCallback && onClick(id)}
|
||||||
|
>
|
||||||
|
<FuseSvgIcon className={clsx('w-full h-full', className, favorite && 'text-secondary-main')}>
|
||||||
|
heroicons-outline:heart
|
||||||
|
</FuseSvgIcon>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(UpdateMark);
|
||||||
25
src/app/main/navigationPages/shared-components/MetaMark.js
Normal file
25
src/app/main/navigationPages/shared-components/MetaMark.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import _ from '@lodash';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
|
||||||
|
function MetaMark({ className, category, status }) {
|
||||||
|
const text = useMemo(
|
||||||
|
() => (status ? `Status: ${_.startCase(status)}` : _.startCase(category)),
|
||||||
|
[category, status]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className={clsx(
|
||||||
|
'flex justify-center align-center px-20 py-2 font-medium border-2 rounded-8',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(MetaMark);
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import _ from '@lodash';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
function PropertiesHeader({ className, categories, layouts, onCategory, onLayout }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'flex items-center gap-44 w-full py-9 px-52 rounded-20 bg-white shadow-light',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="grow flex items-center justify-start gap-16 py-16 border-r-1 border-common-disabled">
|
||||||
|
{categories.map(({ name, amount, active }, idx) => (
|
||||||
|
<button
|
||||||
|
key={name + idx}
|
||||||
|
type="button"
|
||||||
|
className={clsx(
|
||||||
|
'text-2xl text-common-layout cursor-pointer',
|
||||||
|
active && 'text-secondary-main font-semibold cursor-default'
|
||||||
|
)}
|
||||||
|
onClick={() => onCategory(name)}
|
||||||
|
>{`${_.startCase(name)} (${amount})`}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-60 py-16">
|
||||||
|
{layouts.map(({ name, active }, idx) => (
|
||||||
|
<button
|
||||||
|
key={name + idx}
|
||||||
|
type="button"
|
||||||
|
className={clsx(
|
||||||
|
'flex justify-center items-center gap-10 cursor-pointer',
|
||||||
|
active && '!cursor-default'
|
||||||
|
)}
|
||||||
|
onClick={() => onLayout(name)}
|
||||||
|
>
|
||||||
|
<FuseSvgIcon className={clsx('text-common-secondary', active && 'text-secondary-main')}>
|
||||||
|
{`heroicons-outline:view-${name}`}
|
||||||
|
</FuseSvgIcon>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
className={clsx('text-2xl text-common-secondary', active && 'text-secondary-main')}
|
||||||
|
>
|
||||||
|
{_.startCase(name)}
|
||||||
|
</Typography>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(PropertiesHeader);
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import DateMark from './DateMark';
|
||||||
|
import FavoriteButton from './FavoriteButton';
|
||||||
|
import MetaMark from './MetaMark';
|
||||||
|
import StatisticsValue from './StatisticsValue';
|
||||||
|
|
||||||
|
function PropertyGridCard({
|
||||||
|
id,
|
||||||
|
image,
|
||||||
|
title,
|
||||||
|
category,
|
||||||
|
status,
|
||||||
|
update,
|
||||||
|
favorite,
|
||||||
|
statistics,
|
||||||
|
onFavorite,
|
||||||
|
onDelete,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<article className="w-[470px] px-20 pt-20 rounded-20 bg-white shadow-light overflow-hidden">
|
||||||
|
<div className="flex justify-between mb-[25px]">
|
||||||
|
<div className="flex gap-10">
|
||||||
|
<MetaMark
|
||||||
|
category={category}
|
||||||
|
className="text-common-highlight1 border-common-highlight1"
|
||||||
|
/>
|
||||||
|
<MetaMark status={status} className="text-common-highlight2 border-common-highlight2" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-20">
|
||||||
|
<FavoriteButton favorite={favorite} id={id} onClick={onFavorite} />
|
||||||
|
<DateMark update={update} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-start mb-[29px]">
|
||||||
|
<Typography variant="h3" className="mb-[17px] text-3xl font-semibold">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<img src={image} alt={title} className="w-full h-160 rounded-3xl object-cover" />
|
||||||
|
<div className="grid grid-cols-2 justify-between w-full">
|
||||||
|
{statistics.map(({ subject, value, mode }, idx) => (
|
||||||
|
<StatisticsValue
|
||||||
|
key={subject + value + idx}
|
||||||
|
subject={subject}
|
||||||
|
value={value}
|
||||||
|
mode={mode}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-[calc(100%+40px)] -mx-20 border-t-1 border-common-disabled">
|
||||||
|
<button
|
||||||
|
className="flex justify-center items-center gap-10 w-full py-20 border-r-1 border-common-disabled cursor-pointer"
|
||||||
|
type="button"
|
||||||
|
onClick={() => onDelete(id)}
|
||||||
|
>
|
||||||
|
<FuseSvgIcon className="text-common-secondary">heroicons-outline:trash</FuseSvgIcon>
|
||||||
|
<Typography variant="body1" className="text-common-secondary font-medium">
|
||||||
|
Delete
|
||||||
|
</Typography>
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
className="flex justify-center items-center w-full py-[22px] text-lg font-semibold text-secondary-main border-l-1 border-white cursor-pointer"
|
||||||
|
to={`/property/${id}`}
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(PropertyGridCard);
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import DateMark from './DateMark';
|
||||||
|
import FavoriteButton from './FavoriteButton';
|
||||||
|
import MetaMark from './MetaMark';
|
||||||
|
import StatisticsValue from './StatisticsValue';
|
||||||
|
|
||||||
|
function PropertyListItem({
|
||||||
|
id,
|
||||||
|
image,
|
||||||
|
title,
|
||||||
|
category,
|
||||||
|
status,
|
||||||
|
update,
|
||||||
|
favorite,
|
||||||
|
statistics,
|
||||||
|
onFavorite,
|
||||||
|
onDelete,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<article className="flex w-full p-20 rounded-20 bg-white shadow-light overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={title}
|
||||||
|
className="w-[80px] h-[80px] mr-[15px] rounded-3xl object-cover"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mr-20">
|
||||||
|
<div className="flex justify-start gap-60 mb-[22px]">
|
||||||
|
<div className="flex gap-10">
|
||||||
|
<MetaMark
|
||||||
|
category={category}
|
||||||
|
className="text-common-highlight1 border-common-highlight1"
|
||||||
|
/>
|
||||||
|
<MetaMark status={status} className="text-common-highlight2 border-common-highlight2" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-20">
|
||||||
|
<FavoriteButton favorite={favorite} id={id} onClick={onFavorite} />
|
||||||
|
<DateMark update={update} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Typography variant="h3" className="max-w-[480px] text-3xl font-semibold truncate">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grow flex mr-20">
|
||||||
|
{statistics.map(
|
||||||
|
({ subject, value, mode }, idx) =>
|
||||||
|
(idx === 0 || idx === 1) && (
|
||||||
|
<StatisticsValue
|
||||||
|
key={subject + value + idx}
|
||||||
|
subject={subject}
|
||||||
|
value={value}
|
||||||
|
mode={mode}
|
||||||
|
className="-my-[6px]"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col justify-between gap-16">
|
||||||
|
<button
|
||||||
|
className="self-end flex justify-center items-center gap-10 cursor-pointer"
|
||||||
|
type="button"
|
||||||
|
onClick={() => onDelete(id)}
|
||||||
|
>
|
||||||
|
<FuseSvgIcon className="text-common-secondary">heroicons-outline:trash</FuseSvgIcon>
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
className="flex justify-center items-center px-[53px] py-20 -mr-20 -mb-20 text-lg font-semibold text-secondary-main border-l-1 border-t-1 rounded-tl-[20px] border-common-disabled cursor-pointer"
|
||||||
|
to={`/property/${id}`}
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(PropertyListItem);
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { STATISTICS_MODES } from 'app/configs/consts';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
function StatisticsValue({ className, subject, value, mode }) {
|
||||||
|
const isPositive = mode === STATISTICS_MODES.positive;
|
||||||
|
const isExtraPositive = mode === STATISTICS_MODES.extra_positive;
|
||||||
|
const isNegative = mode === STATISTICS_MODES.negative;
|
||||||
|
const isExtraNegative = mode === STATISTICS_MODES.extra_negative;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'max-w-[210px] w-full py-[15px] pl-20 text-left rounded-xl',
|
||||||
|
className,
|
||||||
|
isExtraPositive && 'bg-accept-light',
|
||||||
|
isExtraNegative && 'bg-error-light'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" className="text-lg text-left leading-tight">
|
||||||
|
{subject}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
className={clsx(
|
||||||
|
'text-[28px] font-semibold text-left leading-tight',
|
||||||
|
(isPositive || isExtraPositive) && 'text-accept-main',
|
||||||
|
(isNegative || isExtraNegative) && 'text-error-main'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Typography>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(StatisticsValue);
|
||||||
16
src/app/main/rentAndBuy/RentAndBuy.js
Normal file
16
src/app/main/rentAndBuy/RentAndBuy.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
function RentAndBuy({ t }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col max-w-8xl w-full px-10 py-32 mx-auto">
|
||||||
|
<Typography variant="h1" className="mb-44 text-4xl text-common-layout font-medium">
|
||||||
|
{t('title')}
|
||||||
|
</Typography>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('rentAndBuyPage')(RentAndBuy);
|
||||||
38
src/app/main/rentAndBuy/RentAndBuyConfig.js
Normal file
38
src/app/main/rentAndBuy/RentAndBuyConfig.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { lazy } from 'react';
|
||||||
|
import i18next from 'i18next';
|
||||||
|
|
||||||
|
import RentAndBuy from './RentAndBuy';
|
||||||
|
import en from './i18n/en';
|
||||||
|
|
||||||
|
i18next.addResourceBundle('en', 'rentAndBuyPage', en);
|
||||||
|
|
||||||
|
const SearchAddress = lazy(() => import('./components/SearchAddress/SearchAddress'));
|
||||||
|
const PropertyPreview = lazy(() => import('./components/PropertyPreview/PropertyPreview'));
|
||||||
|
|
||||||
|
const RentAndBuyConfig = {
|
||||||
|
settings: {
|
||||||
|
layout: {
|
||||||
|
config: {},
|
||||||
|
style: 'layout2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: null,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/rent-and-buy',
|
||||||
|
element: <RentAndBuy />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'search',
|
||||||
|
element: <SearchAddress />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'preview',
|
||||||
|
element: <PropertyPreview />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RentAndBuyConfig;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import RegistrationPopup from 'src/app/main/shared-components/popups/RegistrationPopup';
|
||||||
|
|
||||||
|
function PropertyPreview({ t }) {
|
||||||
|
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||||
|
|
||||||
|
const openPopup = () => setIsPopupOpen(true);
|
||||||
|
const closePopup = () => setIsPopupOpen(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
className="w-384 p-20 text-2xl leading-none rounded-lg"
|
||||||
|
aria-label={t('see_more_btn')}
|
||||||
|
type="button"
|
||||||
|
size="large"
|
||||||
|
onClick={openPopup}
|
||||||
|
>
|
||||||
|
{t('see_more_btn')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<RegistrationPopup open={isPopupOpen} onClose={closePopup} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('rentAndBuyPage')(PropertyPreview);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
function SearchAddress({ t }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-68">
|
||||||
|
<span>How are you?</span>
|
||||||
|
<Link
|
||||||
|
to="/rent-and-buy/preview"
|
||||||
|
className="inline-block w-[182px] py-[17px] text-center text-base text-primary-light font-semibold tracking-widest uppercase rounded-lg bg-secondary-light shadow hover:shadow-hover hover:shadow-secondary-light ease-in-out duration-300"
|
||||||
|
>
|
||||||
|
{t('show_btn')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withTranslation('rentAndBuyPage')(SearchAddress);
|
||||||
8
src/app/main/rentAndBuy/i18n/en.js
Normal file
8
src/app/main/rentAndBuy/i18n/en.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
const locale = {
|
||||||
|
title: 'Rent&Buy Analysis',
|
||||||
|
show_btn: 'show',
|
||||||
|
see_more_btn: 'See more',
|
||||||
|
view: 'View the Calculation',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default locale;
|
||||||
11
src/app/main/shared-components/PropertyAnalysisHeader.js
Normal file
11
src/app/main/shared-components/PropertyAnalysisHeader.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
||||||
|
function PropertyAnalysisHeader() {
|
||||||
|
return (
|
||||||
|
<Paper>
|
||||||
|
<div></div>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PropertyAnalysisHeader;
|
||||||
66
src/app/main/shared-components/SearchInput.js
Normal file
66
src/app/main/shared-components/SearchInput.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import _ from '@lodash';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
// import Select from '@mui/material/Select';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { forwardRef, memo, useCallback } from 'react';
|
||||||
|
|
||||||
|
const SEARCH_INPUT_MODES = {
|
||||||
|
simple: 'simple',
|
||||||
|
manual: 'manual',
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledTextField = forwardRef((props, ref) => (
|
||||||
|
<TextField
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
borderRadius: '15px',
|
||||||
|
borderColor: (theme) => theme.palette.secondary.light,
|
||||||
|
borderWidth: '1px',
|
||||||
|
boxShadow: '1px 1px 4px 0px rgba(0, 0, 0, 0.25)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
function SearchInput({ className, mode, placeholder, btnText, query, onType, onSearch }) {
|
||||||
|
const isSimpleMode = mode === SEARCH_INPUT_MODES.simple;
|
||||||
|
const isManualMode = mode === SEARCH_INPUT_MODES.manual;
|
||||||
|
const hasBtn = isSimpleMode || isManualMode;
|
||||||
|
|
||||||
|
const debouncedOnType = useCallback(_.debounce(onType, 250), [onType]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={clsx('flex items-center gap-20', className)}>
|
||||||
|
<StyledTextField
|
||||||
|
type="text"
|
||||||
|
variant="outlined"
|
||||||
|
className="w-full bourder-0"
|
||||||
|
defaultValue={query}
|
||||||
|
placeholder={placeholder ?? ''}
|
||||||
|
onChange={debouncedOnType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* {isManualMode && <Select />} */}
|
||||||
|
|
||||||
|
{hasBtn && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="inherit"
|
||||||
|
className="text-center text-base text-primary-light font-semibold tracking-widest uppercase rounded-2xl bg-secondary-light shadow hover:shadow-hover hover:shadow-secondary-light ease-in-out duration-300"
|
||||||
|
aria-label={btnText ?? ''}
|
||||||
|
type="button"
|
||||||
|
size="large"
|
||||||
|
onClick={onSearch}
|
||||||
|
>
|
||||||
|
{btnText}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(SearchInput);
|
||||||
44
src/app/main/shared-components/popups/BasicPopup.js
Normal file
44
src/app/main/shared-components/popups/BasicPopup.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import Backdrop from '@mui/material/Backdrop';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Fade from '@mui/material/Fade';
|
||||||
|
import Modal from '@mui/material/Modal';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
width: 400,
|
||||||
|
bgcolor: 'inherit',
|
||||||
|
boxShadow: 24,
|
||||||
|
overflow: 'hidden',
|
||||||
|
};
|
||||||
|
|
||||||
|
function BasicPopup({ children, className, open, onClose }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Modal
|
||||||
|
aria-labelledby="transition-modal-title"
|
||||||
|
aria-describedby="transition-modal-description"
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
closeAfterTransition
|
||||||
|
slots={{ backdrop: Backdrop }}
|
||||||
|
slotProps={{
|
||||||
|
backdrop: {
|
||||||
|
timeout: 500,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Fade in={open}>
|
||||||
|
<Box sx={style} className={className}>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</Fade>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(BasicPopup);
|
||||||
72
src/app/main/shared-components/popups/RegistrationPopup.js
Normal file
72
src/app/main/shared-components/popups/RegistrationPopup.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import BasicPopup from './BasicPopup';
|
||||||
|
|
||||||
|
const bullets = [
|
||||||
|
'Lorem ipsum rci egestas. Tortor nulla ac est nulla nisl ut.',
|
||||||
|
'Lorem ipsum dolor sit amet consectetur.',
|
||||||
|
'Lorem ipsum rci egestas. Tortor nulla ac est nulla nisl ut.',
|
||||||
|
'Lorem ipsum dolor sit amet consectetur.',
|
||||||
|
'Lorem ipsum dolor sit amet consectetur.',
|
||||||
|
'Lorem ipsum dt amet consectetur. Duis massa vel estas. Tortor nulla ac est nulla nisl ut.',
|
||||||
|
'Lorem ipsum dolor sit amet consectetur.',
|
||||||
|
];
|
||||||
|
|
||||||
|
function RegistrationPopup({ open, onClose }) {
|
||||||
|
return (
|
||||||
|
<BasicPopup
|
||||||
|
className="flex max-w-[76vw] w-full h-[66vh] min-h-[600px] rounded-20"
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
className="relative hidden md:flex flex-auto items-center justify-center h-full p-64 lg:px-112 overflow-hidden max-w-[45vw] w-full"
|
||||||
|
sx={{ backgroundColor: 'common.layout' }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="absolute inset-0 pointer-events-none"
|
||||||
|
viewBox="0 0 960 540"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
preserveAspectRatio="xMidYMax slice"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
component="g"
|
||||||
|
sx={{ color: 'primary.light' }}
|
||||||
|
className="opacity-20"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="100"
|
||||||
|
>
|
||||||
|
<circle r="234" cx="266" cy="23" />
|
||||||
|
<circle r="234" cx="790" cy="551" />
|
||||||
|
</Box>
|
||||||
|
</svg>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
className="flex flex-col items-center px-60 pt-56"
|
||||||
|
sx={{ backgroundColor: 'background.paper' }}
|
||||||
|
>
|
||||||
|
<Typography variant="h4" className="mb-52 text-4xl font-semibold">
|
||||||
|
Lorem ipsum dolor sit amet consetur
|
||||||
|
</Typography>
|
||||||
|
<ul className="flex flex-col gap-10 mb-68">
|
||||||
|
{bullets.map((bullet) => (
|
||||||
|
<li className="bullet">{bullet}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<Link
|
||||||
|
className="w-full py-20 text-center text-xl text-primary-light font-semibold tracking-widest rounded-2xl bg-secondary-main shadow hover:shadow-hover hover:shadow-secondary-main ease-in-out duration-300"
|
||||||
|
to="/sign-up"
|
||||||
|
>
|
||||||
|
Try for free
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
</BasicPopup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(RegistrationPopup);
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import Checkbox from '@mui/material/Checkbox';
|
|
||||||
import FormControl from '@mui/material/FormControl';
|
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
|
||||||
import TextField from '@mui/material/TextField';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import * as yup from 'yup';
|
|
||||||
import _ from '@lodash';
|
|
||||||
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
|
|
||||||
import AvatarGroup from '@mui/material/AvatarGroup';
|
|
||||||
import Avatar from '@mui/material/Avatar';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import Paper from '@mui/material/Paper';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import jwtService from '../../auth/services/jwtService';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Form Validation Schema
|
|
||||||
*/
|
|
||||||
const schema = yup.object().shape({
|
|
||||||
email: yup.string().email('You must enter a valid email').required('You must enter a email'),
|
|
||||||
password: yup
|
|
||||||
.string()
|
|
||||||
.required('Please enter your password.')
|
|
||||||
.min(4, 'Password is too short - must be at least 4 chars.'),
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultValues = {
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
remember: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
function SignInPage() {
|
|
||||||
const { control, formState, handleSubmit, setError, setValue } = useForm({
|
|
||||||
mode: 'onChange',
|
|
||||||
defaultValues,
|
|
||||||
resolver: yupResolver(schema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { isValid, dirtyFields, errors } = formState;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValue('email', 'admin@fusetheme.com', { shouldDirty: true, shouldValidate: true });
|
|
||||||
setValue('password', 'admin', { shouldDirty: true, shouldValidate: true });
|
|
||||||
}, [setValue]);
|
|
||||||
|
|
||||||
function onSubmit({ email, password }) {
|
|
||||||
jwtService
|
|
||||||
.signInWithEmailAndPassword(email, password)
|
|
||||||
.then((user) => {
|
|
||||||
// No need to do anything, user data will be set at app/auth/AuthContext
|
|
||||||
})
|
|
||||||
.catch((_errors) => {
|
|
||||||
_errors.forEach((error) => {
|
|
||||||
setError(error.type, {
|
|
||||||
type: 'manual',
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-1 min-w-0">
|
|
||||||
<Paper className="h-full sm:h-auto md:flex md:items-center md:justify-end w-full sm:w-auto md:h-full md:w-1/2 py-8 px-16 sm:p-48 md:p-64 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none ltr:border-r-1 rtl:border-l-1">
|
|
||||||
<div className="w-full max-w-320 sm:w-320 mx-auto sm:mx-0">
|
|
||||||
<img className="w-48" src="assets/images/logo/logo.svg" alt="logo" />
|
|
||||||
|
|
||||||
<Typography className="mt-32 text-4xl font-extrabold tracking-tight leading-tight">
|
|
||||||
Sign in
|
|
||||||
</Typography>
|
|
||||||
<div className="flex items-baseline mt-2 font-medium">
|
|
||||||
<Typography>Don't have an account?</Typography>
|
|
||||||
<Link className="ml-4" to="/sign-up">
|
|
||||||
Sign up
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
name="loginForm"
|
|
||||||
noValidate
|
|
||||||
className="flex flex-col justify-center w-full mt-32"
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="email"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
className="mb-24"
|
|
||||||
label="Email"
|
|
||||||
autoFocus
|
|
||||||
type="email"
|
|
||||||
error={!!errors.email}
|
|
||||||
helperText={errors?.email?.message}
|
|
||||||
variant="outlined"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="password"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
className="mb-24"
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
error={!!errors.password}
|
|
||||||
helperText={errors?.password?.message}
|
|
||||||
variant="outlined"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-center sm:justify-between">
|
|
||||||
<Controller
|
|
||||||
name="remember"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControl>
|
|
||||||
<FormControlLabel
|
|
||||||
label="Remember me"
|
|
||||||
control={<Checkbox size="small" {...field} />}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Link className="text-md font-medium" to="/pages/auth/forgot-password">
|
|
||||||
Forgot password?
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
className=" w-full mt-16"
|
|
||||||
aria-label="Sign in"
|
|
||||||
disabled={_.isEmpty(dirtyFields) || !isValid}
|
|
||||||
type="submit"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
Sign in
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex items-center mt-32">
|
|
||||||
<div className="flex-auto mt-px border-t" />
|
|
||||||
<Typography className="mx-8" color="text.secondary">
|
|
||||||
Or continue with
|
|
||||||
</Typography>
|
|
||||||
<div className="flex-auto mt-px border-t" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center mt-32 space-x-16">
|
|
||||||
<Button variant="outlined" className="flex-auto">
|
|
||||||
<FuseSvgIcon size={20} color="action">
|
|
||||||
feather:facebook
|
|
||||||
</FuseSvgIcon>
|
|
||||||
</Button>
|
|
||||||
<Button variant="outlined" className="flex-auto">
|
|
||||||
<FuseSvgIcon size={20} color="action">
|
|
||||||
feather:twitter
|
|
||||||
</FuseSvgIcon>
|
|
||||||
</Button>
|
|
||||||
<Button variant="outlined" className="flex-auto">
|
|
||||||
<FuseSvgIcon size={20} color="action">
|
|
||||||
feather:github
|
|
||||||
</FuseSvgIcon>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
className="relative hidden md:flex flex-auto items-center justify-center h-full p-64 lg:px-112 overflow-hidden"
|
|
||||||
sx={{ backgroundColor: 'primary.main' }}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="absolute inset-0 pointer-events-none"
|
|
||||||
viewBox="0 0 960 540"
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
preserveAspectRatio="xMidYMax slice"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
component="g"
|
|
||||||
sx={{ color: 'primary.light' }}
|
|
||||||
className="opacity-20"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="100"
|
|
||||||
>
|
|
||||||
<circle r="234" cx="196" cy="23" />
|
|
||||||
<circle r="234" cx="790" cy="491" />
|
|
||||||
</Box>
|
|
||||||
</svg>
|
|
||||||
<Box
|
|
||||||
component="svg"
|
|
||||||
className="absolute -top-64 -right-64 opacity-20"
|
|
||||||
sx={{ color: 'primary.light' }}
|
|
||||||
viewBox="0 0 220 192"
|
|
||||||
width="220px"
|
|
||||||
height="192px"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<pattern
|
|
||||||
id="837c3e70-6c3a-44e6-8854-cc48c737b659"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
patternUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<rect x="0" y="0" width="4" height="4" fill="currentColor" />
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect width="220" height="192" fill="url(#837c3e70-6c3a-44e6-8854-cc48c737b659)" />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<div className="z-10 relative w-full max-w-2xl">
|
|
||||||
<div className="text-7xl font-bold leading-none text-gray-100">
|
|
||||||
<div>Welcome to</div>
|
|
||||||
<div>our community</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-24 text-lg tracking-tight leading-6 text-gray-400">
|
|
||||||
Fuse helps developers to build organized and well coded dashboards full of beautiful and
|
|
||||||
rich modules. Join us and start building your application today.
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center mt-32">
|
|
||||||
<AvatarGroup
|
|
||||||
sx={{
|
|
||||||
'& .MuiAvatar-root': {
|
|
||||||
borderColor: 'primary.main',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar src="assets/images/avatars/female-18.jpg" />
|
|
||||||
<Avatar src="assets/images/avatars/female-11.jpg" />
|
|
||||||
<Avatar src="assets/images/avatars/male-09.jpg" />
|
|
||||||
<Avatar src="assets/images/avatars/male-16.jpg" />
|
|
||||||
</AvatarGroup>
|
|
||||||
|
|
||||||
<div className="ml-16 font-medium tracking-tight text-gray-400">
|
|
||||||
More than 17k people joined us, it's your turn
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SignInPage;
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import Checkbox from '@mui/material/Checkbox';
|
|
||||||
import FormControl from '@mui/material/FormControl';
|
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
|
||||||
import TextField from '@mui/material/TextField';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import * as yup from 'yup';
|
|
||||||
import _ from '@lodash';
|
|
||||||
import AvatarGroup from '@mui/material/AvatarGroup';
|
|
||||||
import Avatar from '@mui/material/Avatar';
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import Paper from '@mui/material/Paper';
|
|
||||||
import FormHelperText from '@mui/material/FormHelperText';
|
|
||||||
import jwtService from '../../auth/services/jwtService';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Form Validation Schema
|
|
||||||
*/
|
|
||||||
const schema = yup.object().shape({
|
|
||||||
displayName: yup.string().required('You must enter display name'),
|
|
||||||
email: yup.string().email('You must enter a valid email').required('You must enter a email'),
|
|
||||||
password: yup
|
|
||||||
.string()
|
|
||||||
.required('Please enter your password.')
|
|
||||||
.min(8, 'Password is too short - should be 8 chars minimum.'),
|
|
||||||
passwordConfirm: yup.string().oneOf([yup.ref('password'), null], 'Passwords must match'),
|
|
||||||
acceptTermsConditions: yup.boolean().oneOf([true], 'The terms and conditions must be accepted.'),
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultValues = {
|
|
||||||
displayName: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
passwordConfirm: '',
|
|
||||||
acceptTermsConditions: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
function SignUpPage() {
|
|
||||||
const { control, formState, handleSubmit, reset } = useForm({
|
|
||||||
mode: 'onChange',
|
|
||||||
defaultValues,
|
|
||||||
resolver: yupResolver(schema),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { isValid, dirtyFields, errors, setError } = formState;
|
|
||||||
|
|
||||||
function onSubmit({ displayName, password, email }) {
|
|
||||||
jwtService
|
|
||||||
.createUser({
|
|
||||||
displayName,
|
|
||||||
password,
|
|
||||||
email,
|
|
||||||
})
|
|
||||||
.then((user) => {
|
|
||||||
// No need to do anything, registered user data will be set at app/auth/AuthContext
|
|
||||||
})
|
|
||||||
.catch((_errors) => {
|
|
||||||
_errors.forEach((error) => {
|
|
||||||
setError(error.type, {
|
|
||||||
type: 'manual',
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col sm:flex-row items-center md:items-start sm:justify-center md:justify-start flex-1 min-w-0">
|
|
||||||
<Paper className="h-full sm:h-auto md:flex md:items-center md:justify-end w-full sm:w-auto md:h-full md:w-1/2 py-8 px-16 sm:p-48 md:p-64 sm:rounded-2xl md:rounded-none sm:shadow md:shadow-none ltr:border-r-1 rtl:border-l-1">
|
|
||||||
<div className="w-full max-w-320 sm:w-320 mx-auto sm:mx-0">
|
|
||||||
<img className="w-48" src="assets/images/logo/logo.svg" alt="logo" />
|
|
||||||
|
|
||||||
<Typography className="mt-32 text-4xl font-extrabold tracking-tight leading-tight">
|
|
||||||
Sign up
|
|
||||||
</Typography>
|
|
||||||
<div className="flex items-baseline mt-2 font-medium">
|
|
||||||
<Typography>Already have an account?</Typography>
|
|
||||||
<Link className="ml-4" to="/sign-in">
|
|
||||||
Sign in
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
name="registerForm"
|
|
||||||
noValidate
|
|
||||||
className="flex flex-col justify-center w-full mt-32"
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
name="displayName"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
className="mb-24"
|
|
||||||
label="Display name"
|
|
||||||
autoFocus
|
|
||||||
type="name"
|
|
||||||
error={!!errors.displayName}
|
|
||||||
helperText={errors?.displayName?.message}
|
|
||||||
variant="outlined"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="email"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
className="mb-24"
|
|
||||||
label="Email"
|
|
||||||
type="email"
|
|
||||||
error={!!errors.email}
|
|
||||||
helperText={errors?.email?.message}
|
|
||||||
variant="outlined"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="password"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
className="mb-24"
|
|
||||||
label="Password"
|
|
||||||
type="password"
|
|
||||||
error={!!errors.password}
|
|
||||||
helperText={errors?.password?.message}
|
|
||||||
variant="outlined"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="passwordConfirm"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
className="mb-24"
|
|
||||||
label="Password (Confirm)"
|
|
||||||
type="password"
|
|
||||||
error={!!errors.passwordConfirm}
|
|
||||||
helperText={errors?.passwordConfirm?.message}
|
|
||||||
variant="outlined"
|
|
||||||
required
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="acceptTermsConditions"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControl className="items-center" error={!!errors.acceptTermsConditions}>
|
|
||||||
<FormControlLabel
|
|
||||||
label="I agree to the Terms of Service and Privacy Policy"
|
|
||||||
control={<Checkbox size="small" {...field} />}
|
|
||||||
/>
|
|
||||||
<FormHelperText>{errors?.acceptTermsConditions?.message}</FormHelperText>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
className="w-full mt-24"
|
|
||||||
aria-label="Register"
|
|
||||||
disabled={_.isEmpty(dirtyFields) || !isValid}
|
|
||||||
type="submit"
|
|
||||||
size="large"
|
|
||||||
>
|
|
||||||
Create your free account
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
className="relative hidden md:flex flex-auto items-center justify-center h-full p-64 lg:px-112 overflow-hidden"
|
|
||||||
sx={{ backgroundColor: 'primary.main' }}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="absolute inset-0 pointer-events-none"
|
|
||||||
viewBox="0 0 960 540"
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
preserveAspectRatio="xMidYMax slice"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
component="g"
|
|
||||||
sx={{ color: 'primary.light' }}
|
|
||||||
className="opacity-20"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="100"
|
|
||||||
>
|
|
||||||
<circle r="234" cx="196" cy="23" />
|
|
||||||
<circle r="234" cx="790" cy="491" />
|
|
||||||
</Box>
|
|
||||||
</svg>
|
|
||||||
<Box
|
|
||||||
component="svg"
|
|
||||||
className="absolute -top-64 -right-64 opacity-20"
|
|
||||||
sx={{ color: 'primary.light' }}
|
|
||||||
viewBox="0 0 220 192"
|
|
||||||
width="220px"
|
|
||||||
height="192px"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<pattern
|
|
||||||
id="837c3e70-6c3a-44e6-8854-cc48c737b659"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
patternUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<rect x="0" y="0" width="4" height="4" fill="currentColor" />
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect width="220" height="192" fill="url(#837c3e70-6c3a-44e6-8854-cc48c737b659)" />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<div className="z-10 relative w-full max-w-2xl">
|
|
||||||
<div className="text-7xl font-bold leading-none text-gray-100">
|
|
||||||
<div>Welcome to</div>
|
|
||||||
<div>our community</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-24 text-lg tracking-tight leading-6 text-gray-400">
|
|
||||||
Fuse helps developers to build organized and well coded dashboards full of beautiful and
|
|
||||||
rich modules. Join us and start building your application today.
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center mt-32">
|
|
||||||
<AvatarGroup
|
|
||||||
sx={{
|
|
||||||
'& .MuiAvatar-root': {
|
|
||||||
borderColor: 'primary.main',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar src="assets/images/avatars/female-18.jpg" />
|
|
||||||
<Avatar src="assets/images/avatars/female-11.jpg" />
|
|
||||||
<Avatar src="assets/images/avatars/male-09.jpg" />
|
|
||||||
<Avatar src="assets/images/avatars/male-16.jpg" />
|
|
||||||
</AvatarGroup>
|
|
||||||
|
|
||||||
<div className="ml-16 font-medium tracking-tight text-gray-400">
|
|
||||||
More than 17k people joined us, it's your turn
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SignUpPage;
|
|
||||||
136
src/app/services/authService.js
Normal file
136
src/app/services/authService.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import FuseUtils from '@fuse/utils/FuseUtils';
|
||||||
|
import _ from '@lodash';
|
||||||
|
import * as firebaseAuth from 'firebase/auth';
|
||||||
|
import * as firebaseDb from 'firebase/database';
|
||||||
|
|
||||||
|
export default class AuthService extends FuseUtils.EventEmitter {
|
||||||
|
#auth;
|
||||||
|
|
||||||
|
#db;
|
||||||
|
|
||||||
|
init(authInstance, dbInstance) {
|
||||||
|
this.#auth = authInstance;
|
||||||
|
this.#db = dbInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
createUser = ({ displayName, email, password }) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
firebaseAuth
|
||||||
|
.createUserWithEmailAndPassword(this.#auth, email, password)
|
||||||
|
.then((userCredential) => firebaseAuth.updateProfile(userCredential.user, { displayName }))
|
||||||
|
.then(() => {
|
||||||
|
const userRef = firebaseDb.ref(this.#db, `users/${this.#auth.currentUser.uid}`);
|
||||||
|
const value = { role: 'user', data: { displayName, email }, favorites: [] };
|
||||||
|
|
||||||
|
return firebaseDb.set(userRef, value);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
signInWithEmailAndPassword = ({ email, password, remember }) => {
|
||||||
|
const persistence = remember
|
||||||
|
? firebaseAuth.browserLocalPersistence
|
||||||
|
: firebaseAuth.browserSessionPersistence;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
firebaseAuth
|
||||||
|
.setPersistence(this.#auth, persistence)
|
||||||
|
.then(() => firebaseAuth.signInWithEmailAndPassword(this.#auth, email, password))
|
||||||
|
.then((userCredential) => {
|
||||||
|
resolve(userCredential.user);
|
||||||
|
const { user } = userCredential;
|
||||||
|
this.emit('onLogin', {
|
||||||
|
role: 'user',
|
||||||
|
data: { displayName: user.displayName, photoURL: user.photoURL, email: user.email },
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updateUserData = (user) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (_.isEmpty(user)) {
|
||||||
|
reject(Error('User data is empty'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRef = firebaseDb.ref(this.#db, `users/${this.#auth.currentUser.uid}`);
|
||||||
|
|
||||||
|
firebaseDb
|
||||||
|
.set(userRef, user)
|
||||||
|
.then(() => {
|
||||||
|
if (user.data.email !== this.#auth.currentUser.email) {
|
||||||
|
return firebaseAuth.updateEmail(this.#auth, user.data.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
logout = () => {
|
||||||
|
this.#auth
|
||||||
|
.signOut()
|
||||||
|
.then(() => {
|
||||||
|
this.emit('onLogout', 'Logged out');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('Logout error: ', error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getUserData = (userId) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const userRef = firebaseDb.ref(this.#db, `users/${userId}`);
|
||||||
|
firebaseDb.onValue(
|
||||||
|
userRef,
|
||||||
|
(snapshot) => {
|
||||||
|
const user = snapshot.val();
|
||||||
|
resolve(user);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
sendPasswordResetEmail = (email) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!email) {
|
||||||
|
reject(Error('Email is empty'));
|
||||||
|
}
|
||||||
|
|
||||||
|
firebaseAuth
|
||||||
|
.sendPasswordResetEmail(this.#auth, email)
|
||||||
|
.then(() => {
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onAuthStateChanged = (callback) => {
|
||||||
|
if (!this.#auth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#auth.onAuthStateChanged(callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
15
src/app/services/firebaseService.js
Normal file
15
src/app/services/firebaseService.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { initializeApp } from 'firebase/app';
|
||||||
|
import { getAuth } from 'firebase/auth';
|
||||||
|
import { getDatabase } from 'firebase/database';
|
||||||
|
import { getStorage } from 'firebase/storage';
|
||||||
|
|
||||||
|
export default class FirebaseService {
|
||||||
|
#app;
|
||||||
|
|
||||||
|
constructor(config) {
|
||||||
|
this.app = initializeApp(config);
|
||||||
|
this.auth = getAuth(this.app);
|
||||||
|
this.db = getDatabase(this.app);
|
||||||
|
this.storage = getStorage(this.app);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/app/services/index.js
Normal file
28
src/app/services/index.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import AuthService from './authService';
|
||||||
|
import FirebaseService from './firebaseService';
|
||||||
|
import PropertyService from './propertyService';
|
||||||
|
|
||||||
|
// TODO: change to firebase secrets or to use firebase functions
|
||||||
|
const firebaseConfig = {
|
||||||
|
apiKey: 'AIzaSyBqMGmOF0-DkYDpnsmZwpf5S8w5cL3fBb8',
|
||||||
|
authDomain: 'rental-calculator-13a9e.firebaseapp.com',
|
||||||
|
databaseURL: 'https://rental-calculator-13a9e-default-rtdb.firebaseio.com',
|
||||||
|
projectId: 'rental-calculator-13a9e',
|
||||||
|
storageBucket: 'rental-calculator-13a9e.appspot.com',
|
||||||
|
messagingSenderId: '479612883365',
|
||||||
|
appId: '1:479612883365:web:fde2d2632ce4c42ce5184c',
|
||||||
|
measurementId: 'G-JW7J8ZQ9FJ',
|
||||||
|
};
|
||||||
|
|
||||||
|
// TopHap
|
||||||
|
const propertyConfig = {
|
||||||
|
dataBaseURL: process.env.REACT_APP_PROPERTY_DATA_BASE_URL,
|
||||||
|
widgetBaseURL: process.env.REACT_APP_PROPERTY_WIDGET_BASE_URL,
|
||||||
|
apiKey: process.env.REACT_APP_PROPERTY_API_KEY,
|
||||||
|
};
|
||||||
|
|
||||||
|
const firebase = new FirebaseService(firebaseConfig);
|
||||||
|
const authService = new AuthService();
|
||||||
|
const propertyService = new PropertyService(propertyConfig);
|
||||||
|
|
||||||
|
export { authService, firebase, propertyService };
|
||||||
57
src/app/services/propertyService.js
Normal file
57
src/app/services/propertyService.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const widgets = {
|
||||||
|
absorptionRate: 'absorption-rate',
|
||||||
|
crime: 'crime',
|
||||||
|
hazards: 'hazards',
|
||||||
|
mapPreview: 'map-preview',
|
||||||
|
marketTrends: 'market-trends',
|
||||||
|
noise: 'noise',
|
||||||
|
population: 'population',
|
||||||
|
propertyTypes: 'property-types',
|
||||||
|
rentEstimate: 'rent-estimate',
|
||||||
|
thEstimate: 'th-estimate',
|
||||||
|
turnover: 'turnover',
|
||||||
|
walkability: 'walkability',
|
||||||
|
zipCodeMap: 'zip-code-map',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class PropertyService {
|
||||||
|
#dataApi;
|
||||||
|
|
||||||
|
#widgetApi;
|
||||||
|
|
||||||
|
constructor(config) {
|
||||||
|
const { dataBaseURL, widgetBaseURL, apiKey } = config;
|
||||||
|
this.#dataApi = axios.create({
|
||||||
|
baseURL: dataBaseURL,
|
||||||
|
headers: { 'X-API-Key': apiKey },
|
||||||
|
});
|
||||||
|
this.#widgetApi = axios.create({
|
||||||
|
baseURL: widgetBaseURL,
|
||||||
|
params: {
|
||||||
|
sid: apiKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchProperty(params) {
|
||||||
|
return this.#dataApi.get('/property', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
search(body) {
|
||||||
|
return this.#dataApi.post('/search', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchComparables(params) {
|
||||||
|
return this.#dataApi.get('/comparables', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchWidgetByPropertyId(widget, propertyId, params) {
|
||||||
|
return this.#widgetApi.get(`${widget}/${propertyId}`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchWidgetByAddress(widget, params) {
|
||||||
|
return this.#widgetApi.get(`${widget}/address`, { params });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { forwardRef, useState } from 'react';
|
|
||||||
import { styled } from '@mui/material/styles';
|
|
||||||
import { convertToRaw, EditorState } from 'draft-js';
|
|
||||||
import { Editor } from 'react-draft-wysiwyg';
|
|
||||||
import draftToHtml from 'draftjs-to-html';
|
|
||||||
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
const Root = styled('div')({
|
|
||||||
'& .rdw-dropdown-selectedtext': {
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
'& .rdw-editor-toolbar': {
|
|
||||||
borderWidth: '0 0 1px 0!important',
|
|
||||||
margin: '0!important',
|
|
||||||
},
|
|
||||||
'& .rdw-editor-main': {
|
|
||||||
padding: '8px 12px',
|
|
||||||
height: `${256}px!important`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const WYSIWYGEditor = forwardRef((props, ref) => {
|
|
||||||
const [editorState, setEditorState] = useState(EditorState.createEmpty());
|
|
||||||
|
|
||||||
function onEditorStateChange(_editorState) {
|
|
||||||
setEditorState(_editorState);
|
|
||||||
|
|
||||||
return props.onChange(draftToHtml(convertToRaw(_editorState.getCurrentContent())));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Root className={clsx('rounded-4 border-1 overflow-hidden w-full', props.className)} ref={ref}>
|
|
||||||
<Editor editorState={editorState} onEditorStateChange={onEditorStateChange} />
|
|
||||||
</Root>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default WYSIWYGEditor;
|
|
||||||
@@ -1,53 +1,86 @@
|
|||||||
/* eslint import/no-extraneous-dependencies: off */
|
import browserHistory from '@history';
|
||||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
|
||||||
import history from '@history';
|
|
||||||
import _ from '@lodash';
|
import _ from '@lodash';
|
||||||
import { setInitialSettings } from 'app/store/fuse/settingsSlice';
|
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||||
import { showMessage } from 'app/store/fuse/messageSlice';
|
|
||||||
import settingsConfig from 'app/configs/settingsConfig';
|
import settingsConfig from 'app/configs/settingsConfig';
|
||||||
import jwtService from '../auth/services/jwtService';
|
import { showMessage } from 'app/store/fuse/messageSlice';
|
||||||
|
import { setInitialSettings } from 'app/store/fuse/settingsSlice';
|
||||||
|
import { authService } from '../services';
|
||||||
|
|
||||||
export const setUser = createAsyncThunk('user/setUser', async (user, { dispatch, getState }) => {
|
export const setUser = createAsyncThunk('user/setUser', async (user) => {
|
||||||
/*
|
// You can redirect the logged-in user to a specific route depending on his role
|
||||||
You can redirect the logged-in user to a specific route depending on his role
|
|
||||||
*/
|
|
||||||
if (user.loginRedirectUrl) {
|
if (user.loginRedirectUrl) {
|
||||||
settingsConfig.loginRedirectUrl = user.loginRedirectUrl; // for example 'apps/academy'
|
settingsConfig.loginRedirectUrl = user.loginRedirectUrl; // for example 'apps/academy'
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return _.merge({}, initialState, user);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateUserSettings = createAsyncThunk(
|
export const updateUserSettings = createAsyncThunk(
|
||||||
'user/updateSettings',
|
'user/updateSettings',
|
||||||
async (settings, { dispatch, getState }) => {
|
async (settings, { dispatch, getState }) => {
|
||||||
const { user } = getState();
|
const { user } = getState();
|
||||||
const newUser = _.merge({}, user, { data: { settings } });
|
const newUser = _.omit(_.merge({}, user, { data: { ...settings } }), 'history');
|
||||||
|
|
||||||
dispatch(updateUserData(newUser));
|
dispatch(updateUserData(newUser))
|
||||||
|
.then(() => {
|
||||||
|
dispatch(showMessage({ message: 'User data saved' }));
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
dispatch(showMessage({ message: error.message }));
|
||||||
|
});
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateUserShortcuts = createAsyncThunk(
|
export const updateUserFavorites = createAsyncThunk(
|
||||||
'user/updateShortucts',
|
'user/updateFavorites',
|
||||||
async (shortcuts, { dispatch, getState }) => {
|
async (item, { dispatch, getState }) => {
|
||||||
const { user } = getState();
|
const { user } = getState();
|
||||||
const newUser = {
|
const hasItemInFavorites = user.favorites.find(
|
||||||
...user,
|
(favoriteItem) => favoriteItem.id === item.id && item.favorite
|
||||||
data: {
|
);
|
||||||
...user.data,
|
const hasItemInHistory = user.history.find((history) => history.id === item.id);
|
||||||
shortcuts,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(updateUserData(newUser));
|
const favorites = hasItemInFavorites
|
||||||
|
? user.favorites.filter((favorite) => favorite.id !== item.id)
|
||||||
|
: [...user.favorites, { ...item, favorite: true }];
|
||||||
|
|
||||||
return newUser;
|
if (hasItemInHistory) {
|
||||||
|
const history = user.history.map((historyItem) => {
|
||||||
|
if (historyItem.id === item.id) {
|
||||||
|
return { ...historyItem, favorite: !hasItemInFavorites };
|
||||||
|
}
|
||||||
|
|
||||||
|
return historyItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(updateUserHistory(history));
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUserData = _.omit({ ...user, favorites }, 'history');
|
||||||
|
|
||||||
|
dispatch(updateUserData(newUserData))
|
||||||
|
.then(() => {
|
||||||
|
if (hasItemInFavorites) {
|
||||||
|
dispatch(showMessage({ message: 'The property is removed from favorites' }));
|
||||||
|
} else {
|
||||||
|
dispatch(showMessage({ message: 'The property is saved to favorites' }));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
dispatch(showMessage({ message: error.message }));
|
||||||
|
});
|
||||||
|
|
||||||
|
return favorites;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const updateUserHistory = (history) => (dispatch) => {
|
||||||
|
localStorage.setItem('user', JSON.stringify({ history }));
|
||||||
|
dispatch(userHistoryUpdated(history));
|
||||||
|
};
|
||||||
|
|
||||||
export const logoutUser = () => async (dispatch, getState) => {
|
export const logoutUser = () => async (dispatch, getState) => {
|
||||||
const { user } = getState();
|
const { user } = getState();
|
||||||
|
|
||||||
@@ -56,7 +89,7 @@ export const logoutUser = () => async (dispatch, getState) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
history.push({
|
browserHistory.push({
|
||||||
pathname: '/',
|
pathname: '/',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,24 +104,18 @@ export const updateUserData = (user) => async (dispatch, getState) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtService
|
// eslint-disable-next-line consistent-return
|
||||||
.updateUserData(user)
|
return authService.updateUserData(user);
|
||||||
.then(() => {
|
|
||||||
dispatch(showMessage({ message: 'User data saved with api' }));
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
dispatch(showMessage({ message: error.message }));
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
role: [], // guest
|
role: [], // guest
|
||||||
data: {
|
data: {
|
||||||
displayName: 'John Doe',
|
displayName: 'John Doe',
|
||||||
photoURL: 'assets/images/avatars/brian-hughes.jpg',
|
|
||||||
email: 'johndoe@withinpixels.com',
|
email: 'johndoe@withinpixels.com',
|
||||||
shortcuts: ['apps.calendar', 'apps.mailbox', 'apps.contacts', 'apps.tasks'],
|
|
||||||
},
|
},
|
||||||
|
history: [],
|
||||||
|
favorites: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const userSlice = createSlice({
|
const userSlice = createSlice({
|
||||||
@@ -96,18 +123,21 @@ const userSlice = createSlice({
|
|||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
userLoggedOut: (state, action) => initialState,
|
userLoggedOut: (state, action) => initialState,
|
||||||
|
userHistoryUpdated: (state, action) => ({ ...state, history: action.payload }),
|
||||||
},
|
},
|
||||||
extraReducers: {
|
extraReducers: {
|
||||||
[updateUserSettings.fulfilled]: (state, action) => action.payload,
|
[updateUserSettings.fulfilled]: (state, action) => action.payload,
|
||||||
[updateUserShortcuts.fulfilled]: (state, action) => action.payload,
|
[updateUserFavorites.fulfilled]: (state, action) => ({ ...state, favorites: action.payload }),
|
||||||
[setUser.fulfilled]: (state, action) => action.payload,
|
[setUser.fulfilled]: (state, action) => action.payload,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { userLoggedOut } = userSlice.actions;
|
export const { userLoggedOut, userHistoryUpdated } = userSlice.actions;
|
||||||
|
|
||||||
export const selectUser = ({ user }) => user;
|
export const selectUser = ({ user }) => user;
|
||||||
|
|
||||||
export const selectUserShortcuts = ({ user }) => user.data.shortcuts;
|
export const selectUserHistory = ({ user }) => user.history;
|
||||||
|
|
||||||
|
export const selectUserFavorites = ({ user }) => user.favorites;
|
||||||
|
|
||||||
export default userSlice.reducer;
|
export default userSlice.reducer;
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import FuseDialog from '@fuse/core/FuseDialog';
|
import FuseDialog from '@fuse/core/FuseDialog';
|
||||||
import { styled } from '@mui/material/styles';
|
|
||||||
import FuseMessage from '@fuse/core/FuseMessage';
|
import FuseMessage from '@fuse/core/FuseMessage';
|
||||||
import FuseSuspense from '@fuse/core/FuseSuspense';
|
import FuseSuspense from '@fuse/core/FuseSuspense';
|
||||||
import AppContext from 'app/AppContext';
|
import { styled } from '@mui/material/styles';
|
||||||
|
import Hidden from '@mui/material/Hidden';
|
||||||
|
import AppContext from 'src/app/contexts/AppContext';
|
||||||
|
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
|
||||||
import { memo, useContext } from 'react';
|
import { memo, useContext } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useRoutes } from 'react-router-dom';
|
import { useRoutes } from 'react-router-dom';
|
||||||
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
|
|
||||||
import FooterLayout1 from './components/FooterLayout1';
|
|
||||||
import LeftSideLayout1 from './components/LeftSideLayout1';
|
|
||||||
import NavbarWrapperLayout1 from './components/NavbarWrapperLayout1';
|
import NavbarWrapperLayout1 from './components/NavbarWrapperLayout1';
|
||||||
import RightSideLayout1 from './components/RightSideLayout1';
|
import NavbarToggleButton from '../shared-components/NavbarToggleButton';
|
||||||
import ToolbarLayout1 from './components/ToolbarLayout1';
|
|
||||||
import SettingsPanel from '../shared-components/SettingsPanel';
|
|
||||||
|
|
||||||
const Root = styled('div')(({ theme, config }) => ({
|
const Root = styled('div')(({ theme, config }) => ({
|
||||||
...(config.mode === 'boxed' && {
|
...(config.mode === 'boxed' && {
|
||||||
@@ -37,19 +34,13 @@ function Layout1(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Root id="fuse-layout" config={config} className="w-full flex">
|
<Root id="fuse-layout" config={config} className="w-full flex">
|
||||||
{config.leftSidePanel.display && <LeftSideLayout1 />}
|
|
||||||
|
|
||||||
<div className="flex flex-auto min-w-0">
|
<div className="flex flex-auto min-w-0">
|
||||||
{config.navbar.display && config.navbar.position === 'left' && <NavbarWrapperLayout1 />}
|
{config.navbar.display && config.navbar.position === 'left' && <NavbarWrapperLayout1 />}
|
||||||
|
|
||||||
<main id="fuse-main" className="flex flex-col flex-auto min-h-full min-w-0 relative z-10">
|
<main id="fuse-main" className="flex flex-col flex-auto min-h-full min-w-0 relative z-10">
|
||||||
{config.toolbar.display && (
|
<Hidden lgUp>
|
||||||
<ToolbarLayout1 className={config.toolbar.style === 'fixed' && 'sticky top-0'} />
|
<NavbarToggleButton className="w-40 h-40 p-0 mx-0 sm:mx-8" />
|
||||||
)}
|
</Hidden>
|
||||||
|
|
||||||
<div className="sticky top-0 z-99">
|
|
||||||
<SettingsPanel />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col flex-auto min-h-0 relative z-10">
|
<div className="flex flex-col flex-auto min-h-0 relative z-10">
|
||||||
<FuseDialog />
|
<FuseDialog />
|
||||||
@@ -58,16 +49,9 @@ function Layout1(props) {
|
|||||||
|
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.footer.display && (
|
|
||||||
<FooterLayout1 className={config.footer.style === 'fixed' && 'sticky bottom-0'} />
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{config.navbar.display && config.navbar.position === 'right' && <NavbarWrapperLayout1 />}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{config.rightSidePanel.display && <RightSideLayout1 />}
|
|
||||||
<FuseMessage />
|
<FuseMessage />
|
||||||
</Root>
|
</Root>
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user