Compare commits

..

36 Commits

Author SHA1 Message Date
afed3aed53 Merge pull request 'RC-9-property-service' (#10) from RC-9-property-service into dev
Reviewed-on: #10
2023-10-12 10:31:16 +03:00
ead8f23379 Merge branch 'dev' of https://gitea.a3-global.com/sysadminix/rentalcalculator into RC-9-property-service 2023-10-12 08:29:11 +01:00
4d31f9f71a Merge pull request 'RC-13: create search and buy page' (#9) from RC-13-rent-and-buy-page into dev
Reviewed-on: #9
2023-10-12 10:28:27 +03:00
880df2c2ac RC-13: create search and buy page 2023-10-12 08:20:13 +01:00
c40302aa93 RC-9: create property service 2023-10-07 12:35:40 +01:00
b424376656 Merge pull request 'RC-14: create dashboard page' (#8) from RC-14-dashboard-page into dev
Reviewed-on: #8
2023-09-17 18:31:11 +03:00
788ca3519f RC-14: create dashboard page 2023-09-17 16:30:08 +01:00
3a0f43d491 Merge pull request 'RC-12-registration-popup' (#7) from RC-12-registration-popup into dev
Reviewed-on: #7
2023-08-27 17:44:51 +03:00
6ecb29eb5e RC-12: create registration popup component 2023-08-27 15:43:32 +01:00
96a838eb8e RC-12: create basic popup component 2023-08-27 15:43:20 +01:00
8795de6c7d RC-12: update layout2 2023-08-27 15:43:04 +01:00
5bc0e9220a RC-10: create configs/consts and moved auth roles to that file 2023-08-27 15:42:07 +01:00
d05052b5e3 Merge pull request 'RC-10-favorites-and-history-cards' (#6) from RC-10-favorites-and-history-cards into dev
Reviewed-on: #6
2023-08-27 14:02:14 +03:00
15f9ae928c RC-10: update tailwind config 2023-08-27 12:00:46 +01:00
6983d5724a RC-10: create configs/consts and moved auth roles to that file 2023-08-27 12:00:28 +01:00
36cb82d335 RC-10: update layout1 2023-08-27 11:58:27 +01:00
993bf970d1 RC-10: create history and favorite pages 2023-08-27 11:58:13 +01:00
0db5333242 RC-10: add new values, reducers and selectors for history and favorites to user slice 2023-08-27 11:57:39 +01:00
317617c3ce RC-10: add new values, reducers and selectors for history and favorites to user slice 2023-08-27 11:57:28 +01:00
a5b30367cb RC-10: create shared components for history and favorites pages 2023-08-27 11:56:36 +01:00
e6912e2541 RC-10: add new custom hooks 2023-08-27 11:54:59 +01:00
fdb173e558 RC-7-home-page (#5)
https://ru.yougile.com/team/a605078664af/#chat:2ae00178ba0d
Reviewed-on: #5
2023-08-06 17:46:07 +03:00
efacc0afcf Merge pull request 'RC-8-create-profile-page' (#4) from RC-8-create-profile-page into dev
Reviewed-on: #4
2023-07-12 23:54:02 +03:00
8c9c37cd8d RC-8: add database rules 2023-07-12 21:51:04 +01:00
9469c76a23 RC-8: add global class 2023-07-12 21:50:35 +01:00
3dc66e8fa0 RC-8: create to base 64 util 2023-07-12 21:50:21 +01:00
ae1dba3da9 RC-8: create profile page 2023-07-12 21:50:06 +01:00
e6dfcc8cf7 RC-8: update auth service and user slice 2023-07-12 21:49:07 +01:00
b0d0579ce7 RC-8: correct fuse layout and update theme config 2023-07-12 21:48:28 +01:00
cb85501f7c RC-8: update locale for profile page 2023-07-12 21:47:42 +01:00
8f50650e49 fix: remove acceptTermsConditions from SignUpPage 2023-06-26 18:54:41 +01:00
f56f3f3dc2 fix: remove acceptTermsConditions from SignUpPage 2023-06-26 18:54:03 +01:00
5a680f5f0e Merge pull request 'RC-11-update-config-according-to-ui-kit' (#3) from RC-11-update-config-according-to-ui-kit into dev
Reviewed-on: #3
2023-06-24 22:45:47 +03:00
b78e7b159b RC-11: update components according to ui kit 2023-06-24 20:43:29 +01:00
dbc9bffec4 RC-11: remove useless files 2023-06-24 20:40:41 +01:00
71347c0ace RC-11: update tailwind.config and themeConfig according to ui kit 2023-06-24 20:40:19 +01:00
86 changed files with 2707 additions and 2115 deletions

21
database.rules.json Normal file
View 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
// }
}
}

View File

@@ -1,4 +1,3 @@
/**
* HEY HO
*/
@@ -14,15 +13,15 @@
*,
::before,
::after {
box-sizing: border-box; /* 1 */
border-width: 0; /* 2 */
border-style: solid; /* 2 */
border-color: #EEEEEE; /* 2 */
box-sizing: border-box; /* 1 */
border-width: 0; /* 2 */
border-style: solid; /* 2 */
border-color: #eeeeee; /* 2 */
}
::before,
::after {
--tw-content: '';
--tw-content: '';
}
/*
@@ -33,10 +32,12 @@
*/
html {
line-height: 1.5; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */ /* 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 */
line-height: 1.5; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */ /* 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 */
}
/*
@@ -45,8 +46,8 @@ html {
*/
body {
margin: 0; /* 1 */
line-height: inherit; /* 2 */
margin: 0; /* 1 */
line-height: inherit; /* 2 */
}
/*
@@ -56,9 +57,9 @@ body {
*/
hr {
height: 0; /* 1 */
color: inherit; /* 2 */
border-top-width: 1px; /* 3 */
height: 0; /* 1 */
color: inherit; /* 2 */
border-top-width: 1px; /* 3 */
}
/*
@@ -66,7 +67,7 @@ Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
@@ -79,8 +80,8 @@ h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
font-size: inherit;
font-weight: inherit;
}
/*
@@ -88,8 +89,8 @@ Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
color: inherit;
text-decoration: inherit;
}
/*
@@ -98,7 +99,7 @@ Add the correct font weight in Edge and Safari.
b,
strong {
font-weight: bolder;
font-weight: bolder;
}
/*
@@ -110,8 +111,9 @@ code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */
font-size: 1em; /* 2 */
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace; /* 1 */
font-size: 1em; /* 2 */
}
/*
@@ -119,7 +121,7 @@ Add the correct font size in all browsers.
*/
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,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
bottom: -0.25em;
}
sup {
top: -0.5em;
top: -0.5em;
}
/*
@@ -149,9 +151,9 @@ sup {
*/
table {
text-indent: 0; /* 1 */
border-color: inherit; /* 2 */
border-collapse: collapse; /* 3 */
text-indent: 0; /* 1 */
border-color: inherit; /* 2 */
border-collapse: collapse; /* 3 */
}
/*
@@ -165,12 +167,12 @@ input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: inherit; /* 1 */
color: inherit; /* 1 */
margin: 0; /* 2 */
padding: 0; /* 3 */
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: inherit; /* 1 */
color: inherit; /* 1 */
margin: 0; /* 2 */
padding: 0; /* 3 */
}
/*
@@ -179,7 +181,7 @@ Remove the inheritance of text transform in Edge and Firefox.
button,
select {
text-transform: none;
text-transform: none;
}
/*
@@ -191,9 +193,9 @@ button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button; /* 1 */
background-color: transparent; /* 2 */
background-image: none; /* 2 */
-webkit-appearance: button; /* 1 */
background-color: transparent; /* 2 */
background-image: none; /* 2 */
}
/*
@@ -201,7 +203,7 @@ Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
outline: auto;
}
/*
@@ -209,7 +211,7 @@ Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/
*/
:-moz-ui-invalid {
box-shadow: none;
box-shadow: none;
}
/*
@@ -217,7 +219,7 @@ Add the correct vertical alignment in Chrome and Firefox.
*/
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-outer-spin-button {
height: auto;
height: auto;
}
/*
@@ -235,8 +237,8 @@ Correct the cursor style of increment and decrement buttons in Safari.
*/
[type='search'] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/*
@@ -244,7 +246,7 @@ Remove the inner padding in Chrome and Safari on macOS.
*/
::-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-appearance: button; /* 1 */
font: inherit; /* 2 */
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/*
@@ -262,7 +264,7 @@ Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
display: list-item;
}
/*
@@ -282,24 +284,24 @@ hr,
figure,
p,
pre {
margin: 0;
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
margin: 0;
padding: 0;
}
legend {
padding: 0;
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
list-style: none;
margin: 0;
padding: 0;
}
/*
@@ -307,7 +309,7 @@ Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
resize: vertical;
}
/*
@@ -317,8 +319,8 @@ textarea {
input::placeholder,
textarea::placeholder {
opacity: 1; /* 1 */
color: #BDBDBD; /* 2 */
opacity: 1; /* 1 */
color: #bdbdbd; /* 2 */
}
/*
@@ -326,8 +328,8 @@ Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
[role='button'] {
cursor: pointer;
}
/*
@@ -335,7 +337,7 @@ Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
cursor: default;
}
/*
@@ -352,8 +354,8 @@ audio,
iframe,
embed,
object {
display: block; /* 1 */
vertical-align: middle; /* 2 */
display: block; /* 1 */
vertical-align: middle; /* 2 */
}
/*
@@ -362,8 +364,8 @@ Constrain images and videos to the parent width and preserve their intrinsic asp
img,
video {
max-width: 100%;
height: auto;
max-width: 100%;
height: auto;
}
/*
@@ -371,50 +373,52 @@ Ensure the default browser behavior of the `hidden` attribute.
*/
[hidden] {
display: none;
display: none;
}
*, ::before, ::after {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: '';
--tw-pan-y: '';
--tw-pinch-zoom: '';
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: '';
--tw-slashed-zero: '';
--tw-numeric-figure: '';
--tw-numeric-spacing: '';
--tw-numeric-fraction: '';
--tw-ring-inset: '';
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(33 150 243 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: '';
--tw-brightness: '';
--tw-contrast: '';
--tw-grayscale: '';
--tw-hue-rotate: '';
--tw-invert: '';
--tw-saturate: '';
--tw-sepia: '';
--tw-drop-shadow: '';
--tw-backdrop-blur: '';
--tw-backdrop-brightness: '';
--tw-backdrop-contrast: '';
--tw-backdrop-grayscale: '';
--tw-backdrop-hue-rotate: '';
--tw-backdrop-invert: '';
--tw-backdrop-opacity: '';
--tw-backdrop-saturate: '';
--tw-backdrop-sepia: '';
*,
::before,
::after {
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: '';
--tw-pan-y: '';
--tw-pinch-zoom: '';
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: '';
--tw-slashed-zero: '';
--tw-numeric-figure: '';
--tw-numeric-spacing: '';
--tw-numeric-fraction: '';
--tw-ring-inset: '';
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(33 150 243 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: '';
--tw-brightness: '';
--tw-contrast: '';
--tw-grayscale: '';
--tw-hue-rotate: '';
--tw-invert: '';
--tw-saturate: '';
--tw-sepia: '';
--tw-drop-shadow: '';
--tw-backdrop-blur: '';
--tw-backdrop-brightness: '';
--tw-backdrop-contrast: '';
--tw-backdrop-grayscale: '';
--tw-backdrop-hue-rotate: '';
--tw-backdrop-invert: '';
--tw-backdrop-opacity: '';
--tw-backdrop-saturate: '';
--tw-backdrop-sepia: '';
}

View File

@@ -52,15 +52,15 @@ const inputGlobalStyles = (
// textDecoration: 'none',
// },
// },
'[class*="MuiOutlinedInput-root"]': {
borderRadius: theme.size?.inputRadius,
},
'[class^="border"]': {
borderColor: theme.palette.divider,
},
'[class*="border"]': {
borderColor: theme.palette.divider,
},
// '[class*="MuiOutlinedInput-root"]': {
// borderRadius: `${theme.spacing('10px')}`,
// },
// '[class^="border"]': {
// borderColor: theme.palette.divider,
// },
// '[class*="border"]': {
// borderColor: theme.palette.divider,
// },
'[class*="divide-"] > :not([hidden]) ~ :not([hidden])': {
borderColor: theme.palette.divider,
},

View File

@@ -1,11 +0,0 @@
/**
* Authorization Roles
*/
const authRoles = {
admin: ['admin'],
staff: ['admin', 'staff'],
user: ['admin', 'staff', 'user'],
onlyGuest: [],
};
export default authRoles;

19
src/app/configs/consts.js Normal file
View 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: [],
};

View File

@@ -6,8 +6,9 @@ import Error404Page from '../main/404/Error404Page';
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 = [...navigationPagesConfigs, ...authPagesConfigs, HomeConfig];
const routeConfigs = [...navigationPagesConfigs, ...authPagesConfigs, HomeConfig, RentAndBuyConfig];
const routes = [
...FuseUtils.generateRoutesFromConfigs(routeConfigs, settingsConfig.defaultAuth),

View File

@@ -1,10 +1,7 @@
import { fuseDark, skyBlue } from '@fuse/colors';
import { blueGrey } from '@mui/material/colors';
export const lightPaletteText = {
primary: 'rgb(17, 24, 39)',
secondary: 'rgb(107, 114, 128)',
disabled: 'rgb(149, 156, 169)',
primary: '#151B30',
secondary: '#6D6D6D',
disabled: '#D9D9D9',
};
export const darkPaletteText = {
@@ -22,38 +19,34 @@ const themesConfig = {
common: {
black: 'rgb(17, 24, 39)',
white: 'rgb(255, 255, 255)',
layout: '#141D39',
...lightPaletteText,
},
primary: {
light: '#64748b',
main: '#1e293b',
dark: '#0f172a',
main: '#F1F5F9',
dark: '#F1F1FB',
contrastText: darkPaletteText.primary,
},
secondary: {
light: '#818cf8',
main: '#4f46e5',
light: '#1AD079',
main: '#4D53FF',
dark: '#3730a3',
contrastText: darkPaletteText.primary,
},
background: {
paper: '#FFFFFF',
authPaper: '#F1F5F9',
default: '#f1f5f9',
default: '#F1F5F9',
},
accept: {
light: '#E8FAF2',
main: '#10A75F',
},
error: {
light: '#ffcdd2',
main: '#f44336',
light: '#FBEBEA',
main: '#D83529',
dark: '#b71c1c',
},
border: {
light: '#D9D9D9',
},
},
status: {
danger: 'orange',
},
size: {
inputRadius: '10px',
},
},
defaultDark: {
@@ -79,7 +72,7 @@ const themesConfig = {
},
background: {
paper: '#1e293b',
default: '#111827',
default: '#141D39',
},
error: {
light: '#ffcdd2',
@@ -91,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;

View File

@@ -6,6 +6,63 @@ 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 }) {
@@ -25,14 +82,19 @@ function AuthProvider({ children }) {
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, 'Signed in');
success({ ...user, ...storageUser }, 'Signed in');
} else {
// First login
const { displayName, photoURL, email } = authUser;
success({ role: 'user', data: { displayName, photoURL, email } }, 'Signed in');
success(
{ role: 'user', data: { displayName, photoURL, email }, ...storageUser },
'Signed in'
);
}
})
.catch((error) => {

3
src/app/hooks/index.js Normal file
View File

@@ -0,0 +1,3 @@
export { default as useWindowDimensions } from './useWindowDimensions';
export { default as useOnClickOutside } from './useOnClickOutside';
export { default as usePropertiesHeader } from './usePropertiesHeader';

View 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]);
}

View 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 };
}

View 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;
}

View File

@@ -1,7 +1,7 @@
import i18next from 'i18next';
import ForgotPasswordPage from './ForgotPasswordPage';
import authRoles from '../../../configs/authRoles';
import { authRoles } from '../../../configs/consts';
import en from './i18n/en';
i18next.addResourceBundle('en', 'forgotPasswordPage', en);

View File

@@ -4,6 +4,7 @@ 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';
@@ -15,6 +16,19 @@ 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')),
@@ -43,7 +57,7 @@ function ForgotPasswordPage({ t }) {
<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?.authPaper }}
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">
@@ -63,7 +77,7 @@ function ForgotPasswordPage({ t }) {
name="email"
control={control}
render={({ field }) => (
<TextField
<StyledTextField
{...field}
label={t('email')}
type="email"
@@ -72,16 +86,11 @@ function ForgotPasswordPage({ t }) {
variant="outlined"
required
fullWidth
InputProps={{
sx: {
background: (theme) => theme.palette.background.paper,
},
}}
/>
)}
/>
<div className="flex justify-center w-full">
<div className="flex flex-col items-center justify-center gap-10 w-full">
<Button
variant="contained"
color="secondary"
@@ -93,11 +102,14 @@ function ForgotPasswordPage({ t }) {
>
{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-indigo-400 underline" to="/sign-in">
<Link className="ml-4 text-secondary-main underline" to="/sign-in">
{t('sign_in')}
</Link>
</Typography>

View File

@@ -6,7 +6,7 @@ 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: 'primary.main' }}
sx={{ backgroundColor: 'common.layout' }}
>
<svg
className="absolute inset-0 pointer-events-none"
@@ -30,15 +30,17 @@ function LeftSideCanvas({ title, subtitle, text }) {
</svg>
<div className="z-10 relative w-full max-w-2xl">
{title && <div className="text-7xl font-bold leading-none text-gray-100">{title}</div>}
{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-gray-400">{subtitle}</div>
<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: 'primary.main',
borderColor: 'common.layout',
},
}}
>
@@ -48,7 +50,9 @@ function LeftSideCanvas({ title, subtitle, text }) {
<Avatar src="assets/images/avatars/male-16.jpg" />
</AvatarGroup>
{text && <div className="ml-16 font-medium tracking-tight text-gray-400">{text}</div>}
{text && (
<div className="ml-16 font-medium tracking-tight text-common-disabled">{text}</div>
)}
</div>
</div>
</Box>

View File

@@ -1,7 +1,7 @@
import i18next from 'i18next';
import SignInPage from './SignInPage';
import authRoles from '../../../configs/authRoles';
import { authRoles } from '../../../configs/consts';
import en from './i18n/en';
i18next.addResourceBundle('en', 'signInPage', en);

View File

@@ -7,6 +7,7 @@ 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';
@@ -20,6 +21,19 @@ const defaultValues = {
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')),
@@ -49,7 +63,7 @@ function SignInPage({ t }) {
<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?.authPaper }}
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">
@@ -57,7 +71,7 @@ function SignInPage({ t }) {
</Typography>
<div className="flex items-baseline mt-10 font-medium">
<Typography>{t('have_account')}</Typography>
<Link className="ml-4 text-indigo-400 underline" to="/sign-up">
<Link className="ml-4 text-secondary-main underline" to="/sign-up">
{t('sign_up')}
</Link>
</div>
@@ -72,7 +86,7 @@ function SignInPage({ t }) {
name="email"
control={control}
render={({ field }) => (
<TextField
<StyledTextField
{...field}
className="mb-28"
label={t('email')}
@@ -83,11 +97,6 @@ function SignInPage({ t }) {
variant="outlined"
required
fullWidth
InputProps={{
sx: {
background: (theme) => theme.palette.background.paper,
},
}}
/>
)}
/>
@@ -96,7 +105,7 @@ function SignInPage({ t }) {
name="password"
control={control}
render={({ field }) => (
<TextField
<StyledTextField
{...field}
className="mb-28"
label={t('password')}
@@ -106,11 +115,6 @@ function SignInPage({ t }) {
variant="outlined"
required
fullWidth
InputProps={{
sx: {
background: (theme) => theme.palette.background.paper,
},
}}
/>
)}
/>
@@ -126,7 +130,9 @@ function SignInPage({ t }) {
control={
<Checkbox
{...field}
sx={{ color: (theme) => theme.palette.border.light }}
sx={{
color: (theme) => theme.palette.common.disabled,
}}
/>
}
/>
@@ -134,24 +140,26 @@ function SignInPage({ t }) {
)}
/>
<Link className="text-indigo-400 underline font-medium" to="/forgot-password">
<Link className="text-secondary-main underline font-medium" to="/forgot-password">
{t('forgot_password')}
</Link>
</div>
<div className="flex justify-center w-full">
<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="Sign in"
aria-label={t('sign_in_btn')}
disabled={_.isEmpty(dirtyFields) || !isValid}
type="submit"
size="large"
>
{t('sign_in_btn')}
</Button>
{errors.root && <p>{errors.root.message}</p>}
{errors.root?.message && (
<p className="text-l text-error-main">{errors.root?.message}</p>
)}
</div>
</form>
</div>

View File

@@ -1,7 +1,7 @@
import i18next from 'i18next';
import SignUpPage from './SignUpPage';
import authRoles from '../../../configs/authRoles';
import { authRoles } from '../../../configs/consts';
import en from './i18n/en';
i18next.addResourceBundle('en', 'signUpPage', en);

View File

@@ -1,13 +1,10 @@
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 FormHelperText from '@mui/material/FormHelperText';
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';
@@ -20,16 +17,27 @@ const defaultValues = {
email: '',
password: '',
passwordConfirm: '',
acceptTermsConditions: false,
};
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')),
acceptTermsConditions: yup.boolean().oneOf([true], t('accept_terms_error')),
});
const { control, formState, handleSubmit, setError } = useForm({
@@ -61,7 +69,7 @@ function SignUpPage({ t }) {
<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?.authPaper }}
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">
@@ -69,7 +77,7 @@ function SignUpPage({ t }) {
</Typography>
<div className="flex items-baseline mt-10 font-medium">
<Typography>{t('have_account')}</Typography>
<Link className="ml-4 text-indigo-400 underline" to="/sign-in">
<Link className="ml-4 text-secondary-main underline" to="/sign-in">
{t('sign_in')}
</Link>
</div>
@@ -84,22 +92,17 @@ function SignUpPage({ t }) {
name="name"
control={control}
render={({ field }) => (
<TextField
<StyledTextField
{...field}
className="mb-28"
label={t('name')}
autoFocus
type="name"
type="text"
error={!!errors.name}
helperText={errors?.name?.message}
variant="outlined"
required
fullWidth
InputProps={{
sx: {
background: (theme) => theme.palette.background.paper,
},
}}
/>
)}
/>
@@ -108,7 +111,7 @@ function SignUpPage({ t }) {
name="email"
control={control}
render={({ field }) => (
<TextField
<StyledTextField
{...field}
className="mb-28"
label={t('email')}
@@ -118,11 +121,6 @@ function SignUpPage({ t }) {
variant="outlined"
required
fullWidth
InputProps={{
sx: {
background: (theme) => theme.palette.background.paper,
},
}}
/>
)}
/>
@@ -131,7 +129,7 @@ function SignUpPage({ t }) {
name="password"
control={control}
render={({ field }) => (
<TextField
<StyledTextField
{...field}
className="mb-28"
label={t('password')}
@@ -141,11 +139,6 @@ function SignUpPage({ t }) {
variant="outlined"
required
fullWidth
InputProps={{
sx: {
background: (theme) => theme.palette.background.paper,
},
}}
/>
)}
/>
@@ -154,7 +147,7 @@ function SignUpPage({ t }) {
name="passwordConfirm"
control={control}
render={({ field }) => (
<TextField
<StyledTextField
{...field}
className="mb-28"
label={t('password_confirm')}
@@ -164,32 +157,11 @@ function SignUpPage({ t }) {
variant="outlined"
required
fullWidth
InputProps={{
sx: {
background: (theme) => theme.palette.background.paper,
},
}}
/>
)}
/>
<Controller
name="acceptTermsConditions"
control={control}
render={({ field }) => (
<FormControl className="items-start" error={!!errors.acceptTermsConditions}>
<FormControlLabel
label={t('accept_terms')}
control={
<Checkbox {...field} sx={{ color: (theme) => theme.palette.border.light }} />
}
/>
<FormHelperText>{errors?.acceptTermsConditions?.message}</FormHelperText>
</FormControl>
)}
/>
<div className="flex justify-center w-full">
<div className="flex flex-col items-center justify-center gap-10 w-full">
<Button
variant="contained"
color="secondary"
@@ -201,6 +173,9 @@ function SignUpPage({ t }) {
>
{t('sign_up_btn')}
</Button>
{errors.root?.message && (
<p className="text-l text-error-main">{errors.root?.message}</p>
)}
</div>
</form>
</div>

View File

@@ -18,9 +18,6 @@ const locale = {
password_confirm: 'Password (Confirm)',
password_confirm_error:
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
accept_terms: 'I agree to the Terms of Service and Privacy Policy',
accept_terms_error:
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
sign_up_btn: 'create your account',
};

View File

@@ -1,7 +1,67 @@
import { memo } from 'react';
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';
function Home(props) {
return <div>Heeeeelloooooo!</div>;
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 memo(Home);
export default withTranslation('homePage')(Home);

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

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

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

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

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

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

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

View File

@@ -1,3 +1,41 @@
const locale = {};
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;

View File

@@ -1,7 +1,43 @@
import { styled } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import FusePageSimple from '@fuse/core/FusePageSimple';
import DemoContent from '@fuse/core/DemoContent';
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': {
@@ -16,21 +52,39 @@ const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-sidebarContent': {},
}));
function DashboardPage(props) {
const { t } = useTranslation('dashboardPage');
function DashboardPage({ t }) {
const [query, setQuery] = useState('');
const onInputType = (event) => {
const { target } = event;
const value = target?.value ?? '';
setQuery(value);
};
const onSearch = () => {
// query
};
return (
<Root
header={
<div className="p-24">
<h4>{t('TITLE')}</h4>
</div>
}
content={
<div className="p-24">
<h4>Content</h4>
<br />
<DemoContent />
<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"
@@ -38,4 +92,4 @@ function DashboardPage(props) {
);
}
export default DashboardPage;
export default withTranslation('dashboardPage')(DashboardPage);

View File

@@ -1,6 +1,6 @@
import i18next from 'i18next';
import authRoles from '../../../configs/authRoles';
import { authRoles } from '../../../configs/consts';
import Dashboard from './Dashboard';
import en from './i18n/en';

View File

@@ -1,3 +1,5 @@
const locale = {};
const locale = {
search_input_btn: 'calculate',
};
export default locale;

View File

@@ -1,7 +1,15 @@
import { styled } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import FusePageSimple from '@fuse/core/FusePageSimple';
import DemoContent from '@fuse/core/DemoContent';
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': {
@@ -16,21 +24,73 @@ const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-sidebarContent': {},
}));
function FavoritesPage(props) {
const { t } = useTranslation('favoritesPage');
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
header={
<div className="p-24">
<h4>{t('TITLE')}</h4>
</div>
}
content={
<div className="p-24">
<h4>Content</h4>
<br />
<DemoContent />
<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"

View File

@@ -1,11 +1,13 @@
import { lazy } from 'react';
import i18next from 'i18next';
import authRoles from '../../../configs/authRoles';
import Favorites from './Favorites';
import { authRoles } from '../../../configs/consts';
import en from './i18n/en';
i18next.addResourceBundle('en', 'favoritesPage', en);
const Favorites = lazy(() => import('./Favorites'));
const FavoritesConfig = {
settings: {
layout: {
@@ -22,28 +24,3 @@ const FavoritesConfig = {
};
export default FavoritesConfig;
/**
* Lazy load Example
*/
/*
import React from 'react';
const Example = lazy(() => import('./Example'));
const ExampleConfig = {
settings: {
layout: {
config: {},
},
},
routes: [
{
path: 'example',
element: <Example />,
},
],
};
export default ExampleConfig;
*/

View File

@@ -1,7 +1,15 @@
import { styled } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import FusePageSimple from '@fuse/core/FusePageSimple';
import DemoContent from '@fuse/core/DemoContent';
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': {
@@ -16,21 +24,83 @@ const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-sidebarContent': {},
}));
function HistoryPage(props) {
const { t } = useTranslation('historyPage');
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
header={
<div className="p-24">
<h4>{t('TITLE')}</h4>
</div>
}
content={
<div className="p-24">
<h4>Content</h4>
<br />
<DemoContent />
<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"

View File

@@ -1,11 +1,13 @@
import { lazy } from 'react';
import i18next from 'i18next';
import authRoles from '../../../configs/authRoles';
import History from './History';
import { authRoles } from '../../../configs/consts';
import en from './i18n/en';
i18next.addResourceBundle('en', 'historyPage', en);
const History = lazy(() => import('./History'));
const HistoryConfig = {
settings: {
layout: {
@@ -22,28 +24,3 @@ const HistoryConfig = {
};
export default HistoryConfig;
/**
* Lazy load Example
*/
/*
import React from 'react';
const Example = lazy(() => import('./Example'));
const ExampleConfig = {
settings: {
layout: {
config: {},
},
},
routes: [
{
path: 'example',
element: <Example />,
},
],
};
export default ExampleConfig;
*/

View File

@@ -1,36 +1,378 @@
import { styled } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import FusePageSimple from '@fuse/core/FusePageSimple';
import DemoContent from '@fuse/core/DemoContent';
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': {
backgroundColor: theme.palette.background.paper,
borderBottomWidth: 1,
borderStyle: 'solid',
borderColor: theme.palette.divider,
},
'& .FusePageSimple-header': {},
'& .FusePageSimple-toolbar': {},
'& .FusePageSimple-content': {},
'& .FusePageSimple-content': {
backgroundColor: theme.palette.background.default,
},
'& .FusePageSimple-sidebarHeader': {},
'& .FusePageSimple-sidebarContent': {},
}));
function ProfilePage(props) {
const { t } = useTranslation('profilePage');
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
header={
<div className="p-24">
<h4>{t('TITLE')}</h4>
</div>
}
content={
<div className="p-24">
<h4>Content</h4>
<br />
<DemoContent />
<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"
@@ -38,4 +380,4 @@ function ProfilePage(props) {
);
}
export default ProfilePage;
export default withTranslation('profilePage')(ProfilePage);

View File

@@ -1,11 +1,13 @@
import { lazy } from 'react';
import i18next from 'i18next';
import authRoles from '../../../configs/authRoles';
import { authRoles } from '../../../configs/consts';
import en from './i18n/en';
import Profile from './Profile';
i18next.addResourceBundle('en', 'profilePage', en);
const Profile = lazy(() => import('./Profile'));
const ProfileConfig = {
settings: {
layout: {
@@ -22,28 +24,3 @@ const ProfileConfig = {
};
export default ProfileConfig;
/**
* Lazy load Example
*/
/*
import React from 'react';
const Example = lazy(() => import('./Example'));
const ExampleConfig = {
settings: {
layout: {
config: {},
},
},
routes: [
{
path: 'example',
element: <Example />,
},
],
};
export default ExampleConfig;
*/

View File

@@ -1,3 +1,25 @@
const locale = {};
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;

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

View File

@@ -0,0 +1,11 @@
import Paper from '@mui/material/Paper';
function PropertyAnalysisHeader() {
return (
<Paper>
<div></div>
</Paper>
);
}
export default PropertyAnalysisHeader;

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

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

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

View File

@@ -20,7 +20,7 @@ export default class AuthService extends FuseUtils.EventEmitter {
.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 } };
const value = { role: 'user', data: { displayName, email }, favorites: [] };
return firebaseDb.set(userRef, value);
})
@@ -63,13 +63,12 @@ export default class AuthService extends FuseUtils.EventEmitter {
}
const userRef = firebaseDb.ref(this.#db, `users/${this.#auth.currentUser.uid}`);
const value = { data: { ...user } };
firebaseDb
.set(userRef, value)
.set(userRef, user)
.then(() => {
if (user.email) {
return firebaseAuth.updateEmail(this.#auth, user.email);
if (user.data.email !== this.#auth.currentUser.email) {
return firebaseAuth.updateEmail(this.#auth, user.data.email);
}
return null;
@@ -112,7 +111,7 @@ export default class AuthService extends FuseUtils.EventEmitter {
sendPasswordResetEmail = (email) => {
return new Promise((resolve, reject) => {
if (email) {
if (!email) {
reject(Error('Email is empty'));
}

View File

@@ -1,5 +1,7 @@
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',
@@ -12,7 +14,15 @@ const firebaseConfig = {
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 };
export { authService, firebase, propertyService };

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

View File

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

View File

@@ -1,35 +1,86 @@
/* eslint import/no-extraneous-dependencies: off */
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import history from '@history';
import browserHistory from '@history';
import _ from '@lodash';
import { setInitialSettings } from 'app/store/fuse/settingsSlice';
import { showMessage } from 'app/store/fuse/messageSlice';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import settingsConfig from 'app/configs/settingsConfig';
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 }) => {
/*
You can redirect the logged-in user to a specific route depending on his role
*/
export const setUser = createAsyncThunk('user/setUser', async (user) => {
// You can redirect the logged-in user to a specific route depending on his role
if (user.loginRedirectUrl) {
settingsConfig.loginRedirectUrl = user.loginRedirectUrl; // for example 'apps/academy'
}
return user;
return _.merge({}, initialState, user);
});
export const updateUserSettings = createAsyncThunk(
'user/updateSettings',
async (settings, { dispatch, 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;
}
);
export const updateUserFavorites = createAsyncThunk(
'user/updateFavorites',
async (item, { dispatch, getState }) => {
const { user } = getState();
const hasItemInFavorites = user.favorites.find(
(favoriteItem) => favoriteItem.id === item.id && item.favorite
);
const hasItemInHistory = user.history.find((history) => history.id === item.id);
const favorites = hasItemInFavorites
? user.favorites.filter((favorite) => favorite.id !== item.id)
: [...user.favorites, { ...item, favorite: true }];
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) => {
const { user } = getState();
@@ -38,7 +89,7 @@ export const logoutUser = () => async (dispatch, getState) => {
return null;
}
history.push({
browserHistory.push({
pathname: '/',
});
@@ -53,23 +104,18 @@ export const updateUserData = (user) => async (dispatch, getState) => {
return;
}
authService
.updateUserData(user)
.then(() => {
dispatch(showMessage({ message: 'User data saved' }));
})
.catch((error) => {
dispatch(showMessage({ message: error.message }));
});
// eslint-disable-next-line consistent-return
return authService.updateUserData(user);
};
const initialState = {
role: [], // guest
data: {
displayName: 'John Doe',
photoURL: 'assets/images/avatars/brian-hughes.jpg',
email: 'johndoe@withinpixels.com',
},
history: [],
favorites: [],
};
const userSlice = createSlice({
@@ -77,15 +123,21 @@ const userSlice = createSlice({
initialState,
reducers: {
userLoggedOut: (state, action) => initialState,
userHistoryUpdated: (state, action) => ({ ...state, history: action.payload }),
},
extraReducers: {
[updateUserSettings.fulfilled]: (state, action) => action.payload,
[updateUserFavorites.fulfilled]: (state, action) => ({ ...state, favorites: 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 selectUserHistory = ({ user }) => user.history;
export const selectUserFavorites = ({ user }) => user.favorites;
export default userSlice.reducer;

View File

@@ -2,12 +2,14 @@ import FuseDialog from '@fuse/core/FuseDialog';
import FuseMessage from '@fuse/core/FuseMessage';
import FuseSuspense from '@fuse/core/FuseSuspense';
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 { useSelector } from 'react-redux';
import { useRoutes } from 'react-router-dom';
import NavbarWrapperLayout1 from './components/NavbarWrapperLayout1';
import NavbarToggleButton from '../shared-components/NavbarToggleButton';
const Root = styled('div')(({ theme, config }) => ({
...(config.mode === 'boxed' && {
@@ -36,6 +38,10 @@ function Layout1(props) {
{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">
<Hidden lgUp>
<NavbarToggleButton className="w-40 h-40 p-0 mx-0 sm:mx-8" />
</Hidden>
<div className="flex flex-col flex-auto min-h-0 relative z-10">
<FuseDialog />

View File

@@ -2,10 +2,9 @@ const config = {
title: 'Layout 1 - Dashboard',
defaults: {
mode: 'container',
containerWidth: 1570,
containerWidth: 1590,
navbar: {
display: true,
style: 'style-1',
folded: true,
position: 'left',
},
@@ -55,32 +54,6 @@ const config = {
},
],
},
style: {
title: 'Style',
type: 'radio',
options: [
{
name: 'Slide (style-1)',
value: 'style-1',
},
{
name: 'Folded (style-2)',
value: 'style-2',
},
{
name: 'Tabbed (style-3)',
value: 'style-3',
},
{
name: 'Tabbed Dense (style-3-dense)',
value: 'style-3-dense',
},
],
},
folded: {
title: 'Folded (style-2, style-3)',
type: 'switch',
},
},
},
},

View File

@@ -3,9 +3,7 @@ import { memo } from 'react';
import { useSelector } from 'react-redux';
import { selectFuseCurrentLayoutConfig, selectNavbarTheme } from 'app/store/fuse/settingsSlice';
import { selectFuseNavbar } from 'app/store/fuse/navbarSlice';
import NavbarStyle1 from './navbar/style-1/NavbarStyle1';
import NavbarStyle2 from './navbar/style-2/NavbarStyle2';
import NavbarStyle3 from './navbar/style-3/NavbarStyle3';
import NavbarLayout1 from './navbar/NavbarLayout1';
import NavbarToggleFab from '../../shared-components/NavbarToggleFab';
function NavbarWrapperLayout1(props) {
@@ -17,12 +15,7 @@ function NavbarWrapperLayout1(props) {
return (
<>
<ThemeProvider theme={navbarTheme}>
<>
{config.navbar.style === 'style-1' && <NavbarStyle1 />}
{config.navbar.style === 'style-2' && <NavbarStyle2 />}
{config.navbar.style === 'style-3' && <NavbarStyle3 />}
{config.navbar.style === 'style-3-dense' && <NavbarStyle3 dense />}
</>
<NavbarLayout1 />
</ThemeProvider>
{config.navbar.display && !navbar.open && <NavbarToggleFab />}

View File

@@ -4,9 +4,9 @@ import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import { useDispatch, useSelector } from 'react-redux';
import { navbarCloseMobile, selectFuseNavbar } from 'app/store/fuse/navbarSlice';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import NavbarStyle1Content from './NavbarStyle1Content';
import NavbarLayout1Content from './NavbarLayout1Content';
const navbarWidth = 280;
const navbarWidth = 330;
const StyledNavBar = styled('div')(({ theme, open, position }) => ({
minWidth: navbarWidth,
@@ -40,7 +40,7 @@ const StyledNavBarMobile = styled(SwipeableDrawer)(({ theme }) => ({
},
}));
function NavbarStyle1(props) {
function NavbarLayout1(props) {
const dispatch = useDispatch();
const config = useSelector(selectFuseCurrentLayoutConfig);
const navbar = useSelector(selectFuseNavbar);
@@ -53,7 +53,7 @@ function NavbarStyle1(props) {
open={navbar.open}
position={config.navbar.position}
>
<NavbarStyle1Content />
<NavbarLayout1Content />
</StyledNavBar>
</Hidden>
@@ -72,11 +72,11 @@ function NavbarStyle1(props) {
keepMounted: true, // Better open performance on mobile.
}}
>
<NavbarStyle1Content />
<NavbarLayout1Content />
</StyledNavBarMobile>
</Hidden>
</>
);
}
export default NavbarStyle1;
export default NavbarLayout1;

View File

@@ -2,10 +2,10 @@ import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import { memo } from 'react';
import Logo from '../../../../shared-components/Logo';
import NavbarToggleButton from '../../../../shared-components/NavbarToggleButton';
import UserNavbarHeader from '../../../../shared-components/UserNavbarHeader';
import Navigation from '../../../../shared-components/Navigation';
import Logo from '../../../shared-components/Logo';
import NavbarToggleButton from '../../../shared-components/NavbarToggleButton';
import UserNavbarHeader from '../../../shared-components/UserNavbarHeader';
import Navigation from '../../../shared-components/Navigation';
const Root = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
@@ -32,7 +32,7 @@ const StyledContent = styled(FuseScrollbars)(({ theme }) => ({
backgroundAttachment: 'local, scroll',
}));
function NavbarStyle1Content(props) {
function NavbarLayout1Content(props) {
return (
<Root className={clsx('flex flex-auto flex-col overflow-hidden h-full', props.className)}>
<div className="flex flex-row items-center shrink-0 h-48 md:h-72 px-20">
@@ -50,13 +50,9 @@ function NavbarStyle1Content(props) {
<UserNavbarHeader />
<Navigation layout="vertical" />
<div className="flex flex-0 items-center justify-center py-48 opacity-10">
<img className="w-full max-w-64" src="assets/images/logo/logo.svg" alt="footer logo" />
</div>
</StyledContent>
</Root>
);
}
export default memo(NavbarStyle1Content);
export default memo(NavbarLayout1Content);

View File

@@ -1,171 +0,0 @@
import Hidden from '@mui/material/Hidden';
import { styled } from '@mui/material/styles';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import {
navbarCloseFolded,
navbarCloseMobile,
navbarOpenFolded,
selectFuseNavbar,
} from 'app/store/fuse/navbarSlice';
import { useDispatch, useSelector } from 'react-redux';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import NavbarStyle2Content from './NavbarStyle2Content';
const navbarWidth = 280;
const Root = styled('div')(({ theme, folded }) => ({
display: 'flex',
flexDirection: 'column',
zIndex: 4,
[theme.breakpoints.up('lg')]: {
width: navbarWidth,
minWidth: navbarWidth,
},
...(folded && {
[theme.breakpoints.up('lg')]: {
width: 76,
minWidth: 76,
},
}),
}));
const StyledNavbar = styled('div')(
({ theme, position, folded, foldedandopened, foldedandclosed }) => ({
minWidth: navbarWidth,
width: navbarWidth,
maxWidth: navbarWidth,
maxHeight: '100%',
transition: theme.transitions.create(['width', 'min-width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shorter,
}),
...(position === 'left' && {
left: 0,
}),
...(position === 'right' && {
right: 0,
}),
...(folded && {
position: 'absolute',
width: 76,
minWidth: 76,
top: 0,
bottom: 0,
}),
...(foldedandopened && {
width: navbarWidth,
minWidth: navbarWidth,
}),
...(foldedandclosed && {
'& .NavbarStyle2-content': {
'& .logo-icon': {
width: 44,
height: 44,
},
'& .logo-text': {
opacity: 0,
},
'& .react-badge': {
opacity: 0,
},
'& .fuse-list-item': {
width: 56,
},
'& .fuse-list-item-text, & .arrow-icon, & .item-badge': {
opacity: 0,
},
'& .fuse-list-subheader .fuse-list-subheader-text': {
opacity: 0,
},
'& .fuse-list-subheader:before': {
content: '""',
display: 'block',
position: 'absolute',
minWidth: 16,
borderTop: '2px solid',
opacity: 0.2,
},
'& .collapse-children': {
display: 'none',
},
},
}),
})
);
const StyledNavbarMobile = styled(SwipeableDrawer)(({ theme, position }) => ({
'& > .MuiDrawer-paper': {
minWidth: navbarWidth,
width: navbarWidth,
maxWidth: navbarWidth,
maxHeight: '100%',
transition: theme.transitions.create(['width', 'min-width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shorter,
}),
},
}));
function NavbarStyle2(props) {
const dispatch = useDispatch();
const config = useSelector(selectFuseCurrentLayoutConfig);
const navbar = useSelector(selectFuseNavbar);
// const folded = !navbar.open;
const { folded } = config.navbar;
const foldedandclosed = folded && !navbar.foldedOpen;
const foldedandopened = folded && navbar.foldedOpen;
return (
<Root
folded={folded ? 1 : 0}
open={navbar.open}
id="fuse-navbar"
className="sticky top-0 h-screen shrink-0 z-20 shadow-5"
>
<Hidden lgDown>
<StyledNavbar
className="flex-col flex-auto"
position={config.navbar.position}
folded={folded ? 1 : 0}
foldedandopened={foldedandopened ? 1 : 0}
foldedandclosed={foldedandclosed ? 1 : 0}
onMouseEnter={() => foldedandclosed && dispatch(navbarOpenFolded())}
onMouseLeave={() => foldedandopened && dispatch(navbarCloseFolded())}
>
<NavbarStyle2Content className="NavbarStyle2-content" />
</StyledNavbar>
</Hidden>
<Hidden lgUp>
<StyledNavbarMobile
classes={{
paper: 'flex-col flex-auto h-full',
}}
folded={folded ? 1 : 0}
foldedandopened={foldedandopened ? 1 : 0}
foldedandclosed={foldedandclosed ? 1 : 0}
anchor={config.navbar.position}
variant="temporary"
open={navbar.mobileOpen}
onClose={() => dispatch(navbarCloseMobile())}
onOpen={() => {}}
disableSwipeToOpen
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
<NavbarStyle2Content className="NavbarStyle2-content" />
</StyledNavbarMobile>
</Hidden>
</Root>
);
}
export default NavbarStyle2;

View File

@@ -1,54 +0,0 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled } from '@mui/material/styles';
import clsx from 'clsx';
import { memo } from 'react';
import Logo from '../../../../shared-components/Logo';
import NavbarToggleButton from '../../../../shared-components/NavbarToggleButton';
import Navigation from '../../../../shared-components/Navigation';
const Root = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
'& ::-webkit-scrollbar-thumb': {
boxShadow: `inset 0 0 0 20px ${
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.24)' : 'rgba(255, 255, 255, 0.24)'
}`,
},
'& ::-webkit-scrollbar-thumb:active': {
boxShadow: `inset 0 0 0 20px ${
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.37)' : 'rgba(255, 255, 255, 0.37)'
}`,
},
}));
const StyledContent = styled(FuseScrollbars)(({ theme }) => ({
overscrollBehavior: 'contain',
overflowX: 'hidden',
overflowY: 'auto',
WebkitOverflowScrolling: 'touch',
background:
'linear-gradient(rgba(0, 0, 0, 0) 30%, rgba(0, 0, 0, 0) 30%), linear-gradient(rgba(0, 0, 0, 0.25) 0, rgba(0, 0, 0, 0) 40%)',
backgroundRepeat: 'no-repeat',
backgroundSize: '100% 40px, 100% 10px',
backgroundAttachment: 'local, scroll',
}));
function NavbarStyle2Content(props) {
return (
<Root className={clsx('flex flex-auto flex-col overflow-hidden h-full', props.className)}>
<div className="flex flex-row items-center shrink-0 h-48 md:h-76 px-12">
<div className="flex flex-1 mx-4">
<Logo />
</div>
<NavbarToggleButton className="w-40 h-40 p-0" />
</div>
<StyledContent option={{ suppressScrollX: true, wheelPropagation: false }}>
<Navigation layout="vertical" />
</StyledContent>
</Root>
);
}
export default memo(NavbarStyle2Content);

View File

@@ -1,149 +0,0 @@
import Hidden from '@mui/material/Hidden';
import { styled } from '@mui/material/styles';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import { useDispatch, useSelector } from 'react-redux';
import { navbarCloseMobile, selectFuseNavbar } from 'app/store/fuse/navbarSlice';
import GlobalStyles from '@mui/material/GlobalStyles';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import NavbarStyle3Content from './NavbarStyle3Content';
const navbarWidth = 120;
const navbarWidthDense = 64;
const panelWidth = 280;
const StyledNavBar = styled('div')(({ theme, dense, open, folded, position }) => ({
minWidth: navbarWidth,
width: navbarWidth,
maxWidth: navbarWidth,
...(dense && {
minWidth: navbarWidthDense,
width: navbarWidthDense,
maxWidth: navbarWidthDense,
...(!open && {
...(position === 'left' && {
marginLeft: -navbarWidthDense,
}),
...(position === 'right' && {
marginRight: -navbarWidthDense,
}),
}),
}),
...(!folded && {
minWidth: dense ? navbarWidthDense + panelWidth : navbarWidth + panelWidth,
width: dense ? navbarWidthDense + panelWidth : navbarWidth + panelWidth,
maxWidth: dense ? navbarWidthDense + panelWidth : navbarWidth + panelWidth,
'& #fuse-navbar-panel': {
opacity: '1!important',
pointerEvents: 'initial!important',
},
...(!open && {
...(position === 'left' && {
marginLeft: -(dense ? navbarWidthDense + panelWidth : navbarWidth + panelWidth),
}),
...(position === 'right' && {
marginRight: -(dense ? navbarWidthDense + panelWidth : navbarWidth + panelWidth),
}),
}),
}),
...(!open && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.leavingScreen,
}),
...(position === 'left' && {
marginLeft: -(dense ? navbarWidthDense : navbarWidth),
}),
...(position === 'right' && {
marginRight: -(dense ? navbarWidthDense : navbarWidth),
}),
}),
...(open && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
}),
}));
const StyledNavBarMobile = styled(SwipeableDrawer)(({ theme }) => ({
'& .MuiDrawer-paper': {
'& #fuse-navbar-side-panel': {
minWidth: 'auto',
wdith: 'auto',
},
'& #fuse-navbar-panel': {
opacity: '1!important',
pointerEvents: 'initial!important',
},
},
}));
function NavbarStyle3(props) {
const dispatch = useDispatch();
const config = useSelector(selectFuseCurrentLayoutConfig);
const navbar = useSelector(selectFuseNavbar);
const { folded } = config.navbar;
return (
<>
<GlobalStyles
styles={(theme) => ({
'& #fuse-navbar-side-panel': {
width: props.dense ? navbarWidthDense : navbarWidth,
minWidth: props.dense ? navbarWidthDense : navbarWidth,
maxWidth: props.dense ? navbarWidthDense : navbarWidth,
},
'& #fuse-navbar-panel': {
maxWidth: '100%',
width: panelWidth,
[theme.breakpoints.up('lg')]: {
minWidth: panelWidth,
maxWidth: 'initial',
},
},
})}
/>
<Hidden lgDown>
<StyledNavBar
open={navbar.open}
dense={props.dense ? 1 : 0}
folded={folded ? 1 : 0}
position={config.navbar.position}
className="flex-col flex-auto sticky top-0 h-screen shrink-0 z-20 shadow-5"
>
<NavbarStyle3Content dense={props.dense ? 1 : 0} folded={folded ? 1 : 0} />
</StyledNavBar>
</Hidden>
<Hidden lgUp>
<StyledNavBarMobile
classes={{
paper: 'flex-col flex-auto h-screen max-w-full w-auto overflow-hidden',
}}
anchor={config.navbar.position}
variant="temporary"
open={navbar.mobileOpen}
onClose={() => dispatch(navbarCloseMobile())}
onOpen={() => {}}
disableSwipeToOpen
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
<NavbarStyle3Content dense={props.dense ? 1 : 0} folded={folded ? 1 : 0} />
</StyledNavBarMobile>
</Hidden>
</>
);
}
export default NavbarStyle3;

View File

@@ -1,147 +0,0 @@
import FuseScrollbars from '@fuse/core/FuseScrollbars';
import { styled, useTheme } from '@mui/material/styles';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import clsx from 'clsx';
import { memo, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import FuseNavigation from '@fuse/core/FuseNavigation';
import { navbarCloseMobile } from 'app/store/fuse/navbarSlice';
import { selectContrastMainTheme } from 'app/store/fuse/settingsSlice';
import { useLocation } from 'react-router-dom';
import useThemeMediaQuery from '@fuse/hooks/useThemeMediaQuery';
import { selectNavigation } from 'app/store/fuse/navigationSlice';
const Root = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}));
const StyledPanel = styled(FuseScrollbars)(({ theme, opened }) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
transition: theme.transitions.create(['opacity'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shortest,
}),
opacity: 0,
pointerEvents: 'none',
...(opened && {
opacity: 1,
pointerEvents: 'initial',
}),
}));
function needsToBeOpened(location, item) {
return location && isUrlInChildren(item, location.pathname);
}
function isUrlInChildren(parent, url) {
if (!parent.children) {
return false;
}
for (let i = 0; i < parent.children.length; i += 1) {
if (parent.children[i].children) {
if (isUrlInChildren(parent.children[i], url)) {
return true;
}
}
if (parent.children[i].url === url || url.includes(parent.children[i].url)) {
return true;
}
}
return false;
}
function NavbarStyle3Content(props) {
const isMobile = useThemeMediaQuery((theme) => theme.breakpoints.down('lg'));
const navigation = useSelector(selectNavigation);
const [selectedNavigation, setSelectedNavigation] = useState([]);
const [panelOpen, setPanelOpen] = useState(false);
const theme = useTheme();
const dispatch = useDispatch();
const contrastTheme = useSelector(selectContrastMainTheme(theme.palette.primary.main));
const location = useLocation();
useEffect(() => {
navigation?.forEach((item) => {
if (needsToBeOpened(location, item)) {
setSelectedNavigation([item]);
}
});
}, [navigation, location]);
function handleParentItemClick(selected) {
/** if there is no child item do not set/open panel
*/
if (!selected.children) {
setSelectedNavigation([]);
setPanelOpen(false);
return;
}
/**
* If navigation already selected toggle panel visibility
*/
if (selectedNavigation[0]?.id === selected.id) {
setPanelOpen(!panelOpen);
} else {
/**
* Set navigation and open panel
*/
setSelectedNavigation([selected]);
setPanelOpen(true);
}
}
function handleChildItemClick(selected) {
setPanelOpen(false);
if (isMobile) {
dispatch(navbarCloseMobile());
}
}
return (
<ClickAwayListener onClickAway={() => setPanelOpen(false)}>
<Root className={clsx('flex flex-auto flex h-full', props.className)}>
<div id="fuse-navbar-side-panel" className="flex shrink-0 flex-col items-center">
<img className="w-44 my-32" src="assets/images/logo/logo.svg" alt="logo" />
<FuseScrollbars
className="flex flex-1 min-h-0 justify-center w-full overflow-y-auto overflow-x-hidden"
option={{ suppressScrollX: true, wheelPropagation: false }}
>
<FuseNavigation
className={clsx('navigation')}
navigation={navigation}
layout="vertical-2"
onItemClick={handleParentItemClick}
firstLevel
selectedId={selectedNavigation[0]?.id}
dense={props.dense}
/>
</FuseScrollbars>
</div>
{selectedNavigation.length > 0 && (
<StyledPanel
id="fuse-navbar-panel"
opened={panelOpen}
className={clsx('shadow-5 overflow-y-auto overflow-x-hidden')}
option={{ suppressScrollX: true, wheelPropagation: false }}
>
<FuseNavigation
className={clsx('navigation')}
navigation={selectedNavigation}
layout="vertical"
onItemClick={handleChildItemClick}
/>
</StyledPanel>
)}
</Root>
</ClickAwayListener>
);
}
export default memo(NavbarStyle3Content);

View File

@@ -2,9 +2,9 @@ import FuseSuspense from '@fuse/core/FuseSuspense';
import AppContext from 'src/app/contexts/AppContext';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import i18next from 'i18next';
import { memo, useContext } from 'react';
import { memo, useContext, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useRoutes } from 'react-router-dom';
import { useLocation, useRoutes } from 'react-router-dom';
import { useAuth } from 'src/app/contexts/AuthContext';
import FooterLayout2 from './components/FooterLayout2';
import HeaderLayout2 from './components/HeaderLayout2';
@@ -18,6 +18,16 @@ function Layout2(props) {
const authContext = useAuth();
const appContext = useContext(AppContext);
const { routes } = appContext;
const location = useLocation();
useEffect(() => {
const { hash } = location;
if (hash) {
const target = document.querySelector(hash);
target.scrollIntoView({ behavior: 'smooth' });
}
}, [location]);
return (
<>

View File

@@ -1,13 +1,13 @@
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import linksConfigLayout2 from './linksLayout2Config';
import NavLinks from './NavLinks';
function FooterLayout2() {
const { t } = useTranslation('layout2');
return (
<footer className="flex items-center justify-center w-full bg-gray-900">
<footer className="z-[10000] flex items-center justify-center w-full bg-common-layout">
<div className="flex gap-96 w-full max-w-screen-xl px-10 py-52">
<ul className="flex flex-col gap-16 mr-96">
<li>
@@ -35,14 +35,8 @@ function FooterLayout2() {
</a>
</li>
</ul>
<ul className="flex flex-col gap-16 mt-96">
{linksConfigLayout2.map((path) => (
<li key={path}>
<Link className="text-lg leading-5 text-white no-underline" to={`/${path}`}>
{t(path)}
</Link>
</li>
))}
<ul className="flex flex-col gap-16 mt-[61px]">
<NavLinks className="text-lg leading-5 text-white no-underline" />
</ul>
</div>
</footer>

View File

@@ -2,13 +2,13 @@ import FuseSvgIcon from '@fuse/core/FuseSvgIcon/FuseSvgIcon';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import linksConfigLayout2 from './linksLayout2Config';
import Links from './NavLinks';
function HeaderLayout2(props) {
const { t } = useTranslation('layout2');
return (
<header className="fixed z-50 flex items-center justify-center w-full h-72 px-10 bg-white">
<header className="fixed z-[10000] flex items-center justify-center w-full h-72 px-10 bg-primary-light">
<div className="flex justify-between max-w-screen-xl w-full">
<Link to="/">
<img
@@ -17,24 +17,18 @@ function HeaderLayout2(props) {
alt={t('logo_alt')}
/>
</Link>
<nav className="flex grow justify-center gap-72 items-center">
{linksConfigLayout2.map((path) => (
<Link
className="text-lg leading-5 text-slate-800 no-underline"
to={`/${path}`}
key={path}
>
{t(path)}
</Link>
))}
<Links className="text-lg leading-5 text-common-layout no-underline" />
</nav>
{props.isAuthenticated || (
<div className="flex gap-32 items-center">
<Link className="text-indigo-400" to="/sign-in">
<Link className="text-secondary-main" to="/sign-in">
{t('sign_in')}
</Link>
<Link
className="flex gap-7 items-center px-24 py-10 text-lg leading-5 text-white bg-indigo-400 rounded-2xl"
className="flex gap-7 items-center px-24 py-10 text-lg leading-5 text-primary-light bg-secondary-main rounded-2xl shadow hover:shadow-hover hover:shadow-secondary-main ease-in-out duration-300"
to="/sign-up"
>
<span>{t('sign_up')}</span>

View File

@@ -0,0 +1,26 @@
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
function NavLinks({ className }) {
const { t } = useTranslation('layout2');
return (
<>
<Link className={className} to="/rent-and-buy/search">
{t('rent_and_buy')}
</Link>
<Link className={className} to={{ hash: 'about-us' }}>
{t('about_us')}
</Link>
<Link className={className} to={{ hash: 'blog' }}>
{t('blog')}
</Link>
<Link className={className} to={{ hash: 'contacts' }}>
{t('contacts')}
</Link>
</>
);
}
export default memo(NavLinks);

View File

@@ -1,3 +0,0 @@
const linksConfigLayout2 = ['rent-and-buy', 'about-us', 'blog', 'contacts'];
export default linksConfigLayout2;

View File

@@ -1,6 +1,6 @@
const locale = {
'rent-and-buy': 'Rent&Buy Analysis',
'about-us': 'About Us',
rent_and_buy: 'Rent&Buy Analysis',
about_us: 'About Us',
blog: 'Blog',
contacts: 'Contacts',
sign_in: 'Log In',

7
src/app/utils/index.js Normal file
View File

@@ -0,0 +1,7 @@
export const toBase64 = (value) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(value);
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

View File

@@ -1,5 +0,0 @@
const en = {
translation: {},
};
export default en;

View File

@@ -1,9 +1,12 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './assets/locales/en';
// the translations
// (tip move them in a JSON file and import them)
const resources = {
en,
en: {
translation: {},
},
};
i18n

View File

@@ -1,153 +1,162 @@
/**
* Custom base styles
*/
* {
/* Text rendering */
text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-webkit-text-rendering: optimizeLegibility;
-webkit-tap-highlight-color: transparent;
/* Text rendering */
text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-webkit-text-rendering: optimizeLegibility;
-webkit-tap-highlight-color: transparent;
}
* :focus {
outline: none !important;
outline: none !important;
}
html {
font-size: 62.5%;
font-family: 'Inter var', Roboto, Helvetica Neue, Arial, sans-serif;
background-color: #121212;
font-size: 62.5%;
font-family: 'Inter var', Roboto, Helvetica Neue, Arial, sans-serif;
background-color: #121212;
}
body {
font-size: 14px;
line-height: 1.4;
overflow-x: hidden;
font-feature-settings: "salt";
font-size: 14px;
line-height: normal;
overflow-x: hidden;
font-feature-settings: 'salt';
}
html, body {
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
html,
body {
-webkit-font-smoothing: auto;
-moz-osx-font-smoothing: auto;
}
html, body {
display: flex;
flex-direction: column;
position: relative;
margin: 0;
min-height: 100%;
width: 100%;
flex: 1 1 auto;
html,
body {
display: flex;
flex-direction: column;
position: relative;
margin: 0;
min-height: 100%;
width: 100%;
flex: 1 1 auto;
}
#root {
display: flex;
flex-direction: column;
flex: 1 1 auto;
width: 100%;
height: 100%;
display: flex;
flex: 1 1 auto;
width: 100%;
height: 100%;
}
/* layout 2 workaround */
#root:has(> #fuse-main) {
flex-direction: column;
}
h1, .h1 {
font-size: 24px;
h1,
.h1 {
font-size: 24px;
}
h2, .h2 {
font-size: 20px;
h2,
.h2 {
font-size: 20px;
}
h3, .h3 {
font-size: 16px;
h3,
.h3 {
font-size: 16px;
}
h4, .h4 {
font-size: 15px;
h4,
.h4 {
font-size: 15px;
}
h5, .h5 {
font-size: 13px;
h5,
.h5 {
font-size: 13px;
}
h6, .h6 {
font-size: 12px;
h6,
.h6 {
font-size: 12px;
}
.ps > .ps__rail-y,
.ps > .ps__rail-x {
z-index: 99;
z-index: 99;
}
a[role=button] {
text-decoration: none;
a[role='button'] {
text-decoration: none;
}
[role="tooltip"] {
z-index: 9999;
[role='tooltip'] {
z-index: 9999;
}
.MuiModal-root {
/*z-index: 9999;*/
/*z-index: 9999;*/
}
/* Medium Devices, Desktops Only */
@media only screen and (min-width: 992px) {
::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: rgba(0, 0, 0, 0);
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
background-color: rgba(0, 0, 0, 0);
}
::-webkit-scrollbar:hover {
width: 8px;
height: 8px;
background-color: rgba(0, 0, 0, 0.06);
}
::-webkit-scrollbar:hover {
width: 8px;
height: 8px;
background-color: rgba(0, 0, 0, 0.06);
}
::-webkit-scrollbar-thumb {
border: 2px solid transparent;
border-radius: 20px;
}
::-webkit-scrollbar-thumb {
border: 2px solid transparent;
border-radius: 20px;
}
::-webkit-scrollbar-thumb:active {
border-radius: 20px;
}
::-webkit-scrollbar-thumb:active {
border-radius: 20px;
}
}
form label {
z-index: 99;
z-index: 99;
}
body.no-animate *,
body.no-animate *::before,
body.no-animate *::after {
transition: none !important;
animation: none !important;
transition: none !important;
animation: none !important;
}
button:focus {
outline: none;
outline: none;
}
/* Removes webkit's autofill backgorund color */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
input:-webkit-autofill:active
{
transitionDelay: 9999s;
transitionProperty: background-color, color;
input:-webkit-autofill:active {
transitiondelay: 9999s;
transitionproperty: background-color, color;
}
:focus {
outline-color: transparent;
outline-color: transparent;
}
/*fullcalendar Fix*/
.fc-scrollgrid-section-liquid {
height: 1px !important;
height: 1px !important;
}

View File

@@ -10,3 +10,22 @@
@import 'prism.css';
@tailwind components;
@layer components {
.bullet {
position: relative;
padding-left: 30px;
}
.bullet::before {
content: '';
position: absolute;
transform: translateY(50%);
left: 0;
display: inline-block;
width: 10px;
height: 10px;
background-color: #4d53ff;
border-radius: 50%;
}
}

View File

@@ -2,45 +2,46 @@
/* Print
/*----------------------------------------------------------------*/
@media all {
/* Never show page breaks in normal view */
.page-break-after,
.page-break-before {
display: none;
}
/* Never show page breaks in normal view */
.page-break-after,
.page-break-before {
display: none;
}
}
@media print {
/* html and body tweaks */
html, body {
height: auto !important;
overflow: initial !important;
background: none
}
/* html and body tweaks */
html,
body {
height: auto !important;
overflow: initial !important;
background: none;
}
/* Page breaks */
.page-break-after {
display: block;
page-break-after: always;
position: relative;
}
/* Page breaks */
.page-break-after {
display: block;
page-break-after: always;
position: relative;
}
.page-break-before {
display: block;
page-break-before: always;
position: relative;
}
.page-break-before {
display: block;
page-break-before: always;
position: relative;
}
/* General styles */
#fuse-toolbar,
#fuse-footer,
#fuse-navbar,
#fuse-settings-presets,
#fuse-layout .ps > .ps__rail-x,
#fuse-layout .ps > .ps__rail-y {
display: none !important;
}
/* General styles */
#fuse-toolbar,
#fuse-footer,
#fuse-navbar,
#fuse-settings-presets,
#fuse-layout .ps > .ps__rail-x,
#fuse-layout .ps > .ps__rail-y {
display: none !important;
}
#fuse-layout .ps {
overflow: visible !important;
}
#fuse-layout .ps {
overflow: visible !important;
}
}

View File

@@ -1,212 +1,211 @@
code[class*="language-"],
pre[class*="language-"] {
text-align: left;
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
color: #c3cee3;
background: #263238;
font-family: Roboto Mono,"Liberation Mono",Menlo,Courier,monospace;
font-size: 1em;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
code[class*='language-'],
pre[class*='language-'] {
text-align: left;
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
color: #c3cee3;
background: #263238;
font-family: Roboto Mono, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 1em;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
code[class*="language-"]::-moz-selection,
pre[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection,
pre[class*="language-"] ::-moz-selection {
background: #000000;
code[class*='language-']::-moz-selection,
pre[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection,
pre[class*='language-'] ::-moz-selection {
background: #000000;
}
code[class*="language-"]::selection,
pre[class*="language-"]::selection,
code[class*="language-"] ::selection,
pre[class*="language-"] ::selection {
background: #000000;
code[class*='language-']::selection,
pre[class*='language-']::selection,
code[class*='language-'] ::selection,
pre[class*='language-'] ::selection {
background: #000000;
}
:not(pre) > code[class*="language-"] {
white-space: normal;
border-radius: 0.2em;
padding: 0.1em;
:not(pre) > code[class*='language-'] {
white-space: normal;
border-radius: 0.2em;
padding: 0.1em;
}
pre[class*="language-"] {
overflow: auto;
position: relative;
padding: 12px;
border-radius: 4px;;
pre[class*='language-'] {
overflow: auto;
position: relative;
padding: 12px;
border-radius: 4px;
}
.language-css > code,
.language-sass > code,
.language-scss > code {
color: #fd9170;
color: #fd9170;
}
[class*="language-"] .namespace {
opacity: 0.7;
[class*='language-'] .namespace {
opacity: 0.7;
}
.token.plain-text {
color: #c3cee3;
color: #c3cee3;
}
.token.atrule {
color: #c792ea;
color: #c792ea;
}
.token.attr-name {
color: #ffcb6b;
color: #ffcb6b;
}
.token.attr-value {
color: #c3e88d;
color: #c3e88d;
}
.token.attribute {
color: #c3e88d;
color: #c3e88d;
}
.token.boolean {
color: #c792ea;
color: #c792ea;
}
.token.builtin {
color: #ffcb6b;
color: #ffcb6b;
}
.token.cdata {
color: #80cbc4;
color: #80cbc4;
}
.token.char {
color: #80cbc4;
color: #80cbc4;
}
.token.class {
color: #ffcb6b;
color: #ffcb6b;
}
.token.class-name {
color: #82aaff;
color: #82aaff;
}
.token.color {
color: #f2ff00;
color: #f2ff00;
}
.token.comment {
color: #546e7a;
color: #546e7a;
}
.token.constant {
color: #c792ea;
color: #c792ea;
}
.token.deleted {
color: #f07178;
color: #f07178;
}
.token.doctype {
color: #546e7a;
color: #546e7a;
}
.token.entity {
color: #f07178;
color: #f07178;
}
.token.function {
color: #c792ea;
color: #c792ea;
}
.token.hexcode {
color: #f2ff00;
color: #f2ff00;
}
.token.id {
color: #c792ea;
font-weight: bold;
color: #c792ea;
font-weight: bold;
}
.token.important {
color: #c792ea;
font-weight: bold;
color: #c792ea;
font-weight: bold;
}
.token.inserted {
color: #80cbc4;
color: #80cbc4;
}
.token.keyword {
color: #c792ea;
font-style: italic;
color: #c792ea;
font-style: italic;
}
.token.number {
color: #fd9170;
color: #fd9170;
}
.token.operator {
color: #89ddff;
color: #89ddff;
}
.token.prolog {
color: #546e7a;
color: #546e7a;
}
.token.property {
color: #80cbc4;
color: #80cbc4;
}
.token.pseudo-class {
color: #c3e88d;
color: #c3e88d;
}
.token.pseudo-element {
color: #c3e88d;
color: #c3e88d;
}
.token.punctuation {
color: #89ddff;
color: #89ddff;
}
.token.regex {
color: #f2ff00;
color: #f2ff00;
}
.token.selector {
color: #f07178;
color: #f07178;
}
.token.string {
color: #c3e88d;
color: #c3e88d;
}
.token.symbol {
color: #c792ea;
color: #c792ea;
}
.token.tag {
color: #f07178;
color: #f07178;
}
.token.unit {
color: #f07178;
color: #f07178;
}
.token.url {
color: #fd9170;
color: #fd9170;
}
.token.variable {
color: #f07178;
color: #f07178;
}

View File

@@ -2,68 +2,68 @@
Basic Table Styles
*/
.table-responsive {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
}
table.simple {
width: 100%;
border: none;
border-spacing: 0;
text-align: left;
width: 100%;
border: none;
border-spacing: 0;
text-align: left;
}
table.simple thead tr th {
padding: 16px 8px;
font-weight: 500;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
white-space: nowrap;
padding: 16px 8px;
font-weight: 500;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
white-space: nowrap;
}
table.simple thead tr th:first-child {
padding-left: 24px;
padding-left: 24px;
}
table.simple thead tr th:last-child {
padding-right: 24px;
padding-right: 24px;
}
table.simple tbody tr td {
padding: 12px 8px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
padding: 12px 8px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
table.simple tbody tr td:first-child {
padding-left: 24px;
padding-left: 24px;
}
table.simple tbody tr td:last-child {
padding-right: 24px;
padding-right: 24px;
}
table.simple tbody tr:last-child td {
border-bottom: none;
border-bottom: none;
}
table.simple.clickable tbody tr {
cursor: pointer;
cursor: pointer;
}
table.simple.clickable tbody tr:hover {
background: rgba(0, 0, 0, 0.03);
background: rgba(0, 0, 0, 0.03);
}
table.simple.borderless {
border: none;
border: none;
}
table.simple.borderless tbody tr td{
border: none;
table.simple.borderless tbody tr td {
border: none;
}
table.simple.borderless thead tr th{
border: none;
table.simple.borderless thead tr th {
border: none;
}

View File

@@ -53,6 +53,7 @@ module.exports = {
'gradient-to-bl': 'linear-gradient(to bottom left, var(--tw-gradient-stops))',
'gradient-to-l': 'linear-gradient(to left, var(--tw-gradient-stops))',
'gradient-to-tl': 'linear-gradient(to top left, var(--tw-gradient-stops))',
'home-welcome': 'url(/src/assets/images/welcome-background.webp)',
},
backgroundOpacity: ({ theme }) => theme('opacity'),
backgroundPosition: {
@@ -130,6 +131,9 @@ module.exports = {
'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
inner: 'inset 0 2px 4px 0 rgba(0,0,0,0.06)',
none: 'none',
light: '1px 1px 5px 0px rgba(0, 0, 0, 0.1)',
active: '2px 2px 5px 0px rgba(0, 0, 0, 0.2)',
hover: '2px 2px 5px 0px rgba(0, 0, 0, 0.4)',
0: '0px 0px 0px 0px rgba(0, 0, 0, 0.2), 0px 0px 0px 0px rgba(0, 0, 0, 0.14), 0px 0px 0px 0px rgba(0, 0, 0, 0.12)',
1: '0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12)',
2: '0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12)',
@@ -176,7 +180,32 @@ module.exports = {
current: colors.current,
transparent: 'transparent',
black: '#22292F',
white: '#fff',
white: '#FFFFFF',
common: {
layout: '#141D39',
primary: '#151B30',
secondary: '#6D6D6D',
disabled: '#D9D9D9',
highlight1: '#FFBC6E',
highlight2: '#DB00FF',
},
primary: {
light: '#FFFFFF',
main: '#F1F5F9',
dark: '#F1F1FB',
},
secondary: {
light: '#1AD079',
main: '#4D53FF',
},
accept: {
light: '#E8FAF2',
main: '#10A75F',
},
error: {
light: '#FBEBEA',
main: '#D83529',
},
grey: {
50: '#FAFAFA',
100: '#F5F5F5',
@@ -1233,6 +1262,7 @@ module.exports = {
'5xl': '102.4rem',
'6xl': '115.2rem',
'7xl': '128rem',
'8xl': '142rem',
px: '1px',
0: '0px',
0.5: '0.05rem',