Compare commits
28 Commits
b0d0579ce7
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| afed3aed53 | |||
| ead8f23379 | |||
| 4d31f9f71a | |||
| 880df2c2ac | |||
| c40302aa93 | |||
| b424376656 | |||
| 788ca3519f | |||
| 3a0f43d491 | |||
| 6ecb29eb5e | |||
| 96a838eb8e | |||
| 8795de6c7d | |||
| 5bc0e9220a | |||
| d05052b5e3 | |||
| 15f9ae928c | |||
| 6983d5724a | |||
| 36cb82d335 | |||
| 993bf970d1 | |||
| 0db5333242 | |||
| 317617c3ce | |||
| a5b30367cb | |||
| e6912e2541 | |||
| fdb173e558 | |||
| efacc0afcf | |||
| 8c9c37cd8d | |||
| 9469c76a23 | |||
| 3dc66e8fa0 | |||
| ae1dba3da9 | |||
| e6dfcc8cf7 |
21
database.rules.json
Normal file
21
database.rules.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"rules": {
|
||||
".read": false,
|
||||
".write": false,
|
||||
"users": {
|
||||
"$userId": {
|
||||
".read": "auth.uid === $userId",
|
||||
".write": "auth.uid === $userId"
|
||||
}
|
||||
}
|
||||
// // readable node
|
||||
// "messages": {
|
||||
// ".read": true
|
||||
// },
|
||||
// // readable and writable node
|
||||
// "messages": {
|
||||
// ".read": true,
|
||||
// ".write": true
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* HEY HO
|
||||
*/
|
||||
@@ -17,7 +16,7 @@
|
||||
box-sizing: border-box; /* 1 */
|
||||
border-width: 0; /* 2 */
|
||||
border-style: solid; /* 2 */
|
||||
border-color: #EEEEEE; /* 2 */
|
||||
border-color: #eeeeee; /* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
@@ -36,7 +35,9 @@ 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 */
|
||||
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 */
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -110,7 +111,8 @@ code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||
'Courier New', monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
@@ -318,7 +320,7 @@ textarea {
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1; /* 1 */
|
||||
color: #BDBDBD; /* 2 */
|
||||
color: #bdbdbd; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -326,7 +328,7 @@ Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
[role='button'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -374,7 +376,9 @@ Ensure the default browser behavior of the `hidden` attribute.
|
||||
display: none;
|
||||
}
|
||||
|
||||
*, ::before, ::after {
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
|
||||
@@ -52,15 +52,15 @@ const inputGlobalStyles = (
|
||||
// textDecoration: 'none',
|
||||
// },
|
||||
// },
|
||||
'[class*="MuiOutlinedInput-root"]': {
|
||||
borderRadius: `${theme.spacing('10px')}!important`,
|
||||
},
|
||||
'[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,
|
||||
},
|
||||
|
||||
@@ -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
19
src/app/configs/consts.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export const STATISTICS_MODES = {
|
||||
positive: 'positive',
|
||||
extra_positive: 'extra_positive',
|
||||
negative: 'negative',
|
||||
extra_negative: 'extra_negative',
|
||||
};
|
||||
|
||||
export const PROPERTIES_LAYOUTS = {
|
||||
list: 'list',
|
||||
grid: 'grid',
|
||||
};
|
||||
|
||||
// Authorization Roles
|
||||
export const authRoles = {
|
||||
admin: ['admin'],
|
||||
staff: ['admin', 'staff'],
|
||||
user: ['admin', 'staff', 'user'],
|
||||
onlyGuest: [],
|
||||
};
|
||||
@@ -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),
|
||||
|
||||
@@ -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
3
src/app/hooks/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as useWindowDimensions } from './useWindowDimensions';
|
||||
export { default as useOnClickOutside } from './useOnClickOutside';
|
||||
export { default as usePropertiesHeader } from './usePropertiesHeader';
|
||||
29
src/app/hooks/useOnClickOutside.js
Normal file
29
src/app/hooks/useOnClickOutside.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function useOnClickOutside(ref, handler) {
|
||||
useEffect(() => {
|
||||
const onEvent = (event) => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event instanceof KeyboardEvent && event.key === 'Escape') {
|
||||
handler(event);
|
||||
} else if (ref.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler(event);
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', onEvent);
|
||||
document.addEventListener('touchstart', onEvent);
|
||||
document.addEventListener('keydown', onEvent);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onEvent);
|
||||
document.removeEventListener('touchstart', onEvent);
|
||||
document.removeEventListener('keydown', onEvent);
|
||||
};
|
||||
}, [ref, handler]);
|
||||
}
|
||||
105
src/app/hooks/usePropertiesHeader.js
Normal file
105
src/app/hooks/usePropertiesHeader.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import { PROPERTIES_LAYOUTS } from 'app/configs/consts';
|
||||
import { useState, useMemo, useCallback, useEffect } from 'react';
|
||||
|
||||
export default function usePropertiesHeader(items) {
|
||||
const [categories, setCategories] = useState([
|
||||
{
|
||||
name: 'all',
|
||||
amount: 0,
|
||||
active: true,
|
||||
},
|
||||
]);
|
||||
const [layouts, setLayouts] = useState([
|
||||
{ name: PROPERTIES_LAYOUTS.list, active: true },
|
||||
{ name: PROPERTIES_LAYOUTS.grid, active: false },
|
||||
]);
|
||||
|
||||
const activeCategory = useMemo(
|
||||
() => categories.find(({ active }) => active)?.name ?? categories[0].name,
|
||||
[categories]
|
||||
);
|
||||
const activeLayout = useMemo(
|
||||
() => layouts.find(({ active }) => active)?.name ?? PROPERTIES_LAYOUTS.list,
|
||||
[layouts]
|
||||
);
|
||||
|
||||
const onCategory = useCallback(
|
||||
(value) => {
|
||||
if (value === activeCategory) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCategories((prevState) =>
|
||||
prevState.map((category) => ({ ...category, active: category.name === value }))
|
||||
);
|
||||
},
|
||||
[activeCategory]
|
||||
);
|
||||
|
||||
const onLayout = useCallback(
|
||||
(value) => {
|
||||
if (value === activeLayout) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLayouts((prevState) => prevState.map(({ name }) => ({ name, active: name === value })));
|
||||
},
|
||||
[activeLayout]
|
||||
);
|
||||
|
||||
const onItemDelete = (itemCategory) => {
|
||||
setCategories((prevState) => {
|
||||
const isItemCategoryLast =
|
||||
prevState.find((category) => category.name === itemCategory)?.amount === 1;
|
||||
|
||||
return prevState
|
||||
.map((category, idx) => {
|
||||
if (!idx) {
|
||||
return {
|
||||
name: category.name,
|
||||
amount: category.amount - 1,
|
||||
active: isItemCategoryLast,
|
||||
};
|
||||
}
|
||||
|
||||
if (category?.name === itemCategory) {
|
||||
if (category.amount > 1) {
|
||||
return { ...category, amount: category.amount - 1 };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return category;
|
||||
})
|
||||
.filter((category) => category);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let updatedCategories = [...categories];
|
||||
items.forEach((item) => {
|
||||
const hasItemCategory = updatedCategories.find((category) => category.name === item.category);
|
||||
updatedCategories = updatedCategories.map((category, idx) => {
|
||||
if (!idx) {
|
||||
category.amount += 1;
|
||||
}
|
||||
|
||||
return category;
|
||||
});
|
||||
|
||||
if (hasItemCategory) {
|
||||
updatedCategories = updatedCategories.map((category) => ({
|
||||
...category,
|
||||
amount: item.category === category.name ? category.amount + 1 : category.amount,
|
||||
}));
|
||||
} else {
|
||||
updatedCategories.push({ name: item.category, amount: 1, active: false });
|
||||
}
|
||||
});
|
||||
setCategories(updatedCategories);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return { categories, activeCategory, onCategory, layouts, activeLayout, onLayout, onItemDelete };
|
||||
}
|
||||
20
src/app/hooks/useWindowDimensions.js
Normal file
20
src/app/hooks/useWindowDimensions.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function useWindowDimensions() {
|
||||
const getWindowDimensions = () => {
|
||||
const { innerWidth: width, innerHeight: height } = window;
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
|
||||
|
||||
useEffect(() => {
|
||||
const onRecize = () => setWindowDimensions(getWindowDimensions());
|
||||
|
||||
window.addEventListener('resize', onRecize);
|
||||
|
||||
return () => window.removeEventListener('resize', onRecize);
|
||||
}, []);
|
||||
|
||||
return windowDimensions;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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')),
|
||||
@@ -63,7 +77,7 @@ function ForgotPasswordPage({ t }) {
|
||||
name="email"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
<StyledTextField
|
||||
{...field}
|
||||
label={t('email')}
|
||||
type="email"
|
||||
@@ -72,11 +86,6 @@ function ForgotPasswordPage({ t }) {
|
||||
variant="outlined"
|
||||
required
|
||||
fullWidth
|
||||
InputProps={{
|
||||
sx: {
|
||||
background: (theme) => theme.palette.background.paper,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')),
|
||||
@@ -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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -146,7 +150,7 @@ function SignInPage({ t }) {
|
||||
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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
@@ -18,6 +19,19 @@ const defaultValues = {
|
||||
passwordConfirm: '',
|
||||
};
|
||||
|
||||
const StyledTextField = forwardRef((props, ref) => (
|
||||
<TextField
|
||||
InputProps={{
|
||||
sx: {
|
||||
background: (theme) => theme.palette.background.paper,
|
||||
borderRadius: '10px',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
|
||||
function SignUpPage({ t }) {
|
||||
const schema = yup.object().shape({
|
||||
name: yup.string().required(t('name_error')),
|
||||
@@ -78,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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -102,7 +111,7 @@ function SignUpPage({ t }) {
|
||||
name="email"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
<StyledTextField
|
||||
{...field}
|
||||
className="mb-28"
|
||||
label={t('email')}
|
||||
@@ -112,11 +121,6 @@ function SignUpPage({ t }) {
|
||||
variant="outlined"
|
||||
required
|
||||
fullWidth
|
||||
InputProps={{
|
||||
sx: {
|
||||
background: (theme) => theme.palette.background.paper,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -125,7 +129,7 @@ function SignUpPage({ t }) {
|
||||
name="password"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
<StyledTextField
|
||||
{...field}
|
||||
className="mb-28"
|
||||
label={t('password')}
|
||||
@@ -135,11 +139,6 @@ function SignUpPage({ t }) {
|
||||
variant="outlined"
|
||||
required
|
||||
fullWidth
|
||||
InputProps={{
|
||||
sx: {
|
||||
background: (theme) => theme.palette.background.paper,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -148,7 +147,7 @@ function SignUpPage({ t }) {
|
||||
name="passwordConfirm"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
<StyledTextField
|
||||
{...field}
|
||||
className="mb-28"
|
||||
label={t('password_confirm')}
|
||||
@@ -158,11 +157,6 @@ function SignUpPage({ t }) {
|
||||
variant="outlined"
|
||||
required
|
||||
fullWidth
|
||||
InputProps={{
|
||||
sx: {
|
||||
background: (theme) => theme.palette.background.paper,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
37
src/app/main/home/components/AboutUs.js
Normal file
37
src/app/main/home/components/AboutUs.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { memo } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
function AboutUs({ t }) {
|
||||
return (
|
||||
<section id="about-us" className="flex flex-col items-center pt-72 mb-80">
|
||||
<h2 className="self-start mb-56 text-[48px] font-semibold">{t('about_us_title')}</h2>
|
||||
<div className="flex gap-64 mb-[126px]">
|
||||
<div className="flex items-center">
|
||||
<iframe
|
||||
className="rounded-20"
|
||||
width="715"
|
||||
height="402"
|
||||
src="https://www.youtube.com/embed/rNSIwjmynYQ?controls=0"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
<aside className="flex flex-col items-center py-40 px-52 bg-primary-light rounded-20">
|
||||
<h3 className="mb-16 text-lg text-common-layout font-medium">{t('about_us_subject')}</h3>
|
||||
<p className="mb-16 text-lg text-common-layout font-light">{t('about_us_text_1')}</p>
|
||||
<p className="mb-16 text-lg text-common-layout font-light">{t('about_us_text_2')}</p>
|
||||
</aside>
|
||||
</div>
|
||||
<Link
|
||||
className="w-[220px] py-[17px] text-center text-base text-primary-light font-semibold tracking-widest uppercase rounded-2xl bg-secondary-light shadow hover:shadow-hover hover:shadow-secondary-light ease-in-out duration-300"
|
||||
to="/rent-and-buy/search"
|
||||
>
|
||||
{t('research_btn')}
|
||||
</Link>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTranslation('homePage')(memo(AboutUs));
|
||||
33
src/app/main/home/components/ArticleCard.js
Normal file
33
src/app/main/home/components/ArticleCard.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { memo } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
function ArticleCard({ t, id, title, description, image, updated }) {
|
||||
return (
|
||||
<article className="flex flex-col justify-between max-w-[460px] w-full h-[526px] bg-primary-light rounded-20 shadow-light">
|
||||
<div>
|
||||
<img
|
||||
className="w-full h-[230px] mb-20 rounded-20 object-cover"
|
||||
src={image}
|
||||
alt={title}
|
||||
width="460"
|
||||
height="230"
|
||||
loading="lazy"
|
||||
/>
|
||||
<h3 className="px-[15px] mb-20 text-xl leading-light font-medium">{title}</h3>
|
||||
<p className="px-[15px] text-lg text-common-layout font-light">{description}</p>
|
||||
</div>
|
||||
<div className="px-[15px] mb-[17px] flex justify-between items-center">
|
||||
<p className="text-lg text-common-secondary font-light">{updated}</p>
|
||||
<Link
|
||||
className="flex items-center justify-center w-[60px] h-[60px] text-secondary-main rounded-full border-2 border-secondary-main"
|
||||
to={`/blog/${id}`}
|
||||
>
|
||||
{t('article_btn')}
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTranslation('homePage')(memo(ArticleCard));
|
||||
17
src/app/main/home/components/ArticleCardsList.js
Normal file
17
src/app/main/home/components/ArticleCardsList.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { memo } from 'react';
|
||||
import ArticleCard from './ArticleCard';
|
||||
|
||||
function ArticleCardsList({ title, cards, ...attrs }) {
|
||||
return (
|
||||
<section {...attrs}>
|
||||
<h2 className="mb-56 text-[48px] font-semibold">{title}</h2>
|
||||
<div className="flex gap-20">
|
||||
{cards.map((card) => (
|
||||
<ArticleCard {...card} key={card.id} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ArticleCardsList);
|
||||
157
src/app/main/home/components/FeedbackForm.js
Normal file
157
src/app/main/home/components/FeedbackForm.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import _ from '@lodash';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { forwardRef, memo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import * as yup from 'yup';
|
||||
|
||||
const defaultValues = {
|
||||
name: '',
|
||||
email: '',
|
||||
message: '',
|
||||
};
|
||||
|
||||
const StyledTextField = forwardRef((props, ref) => (
|
||||
<TextField
|
||||
InputProps={{
|
||||
sx: {
|
||||
background: (theme) => theme.palette.background.paper,
|
||||
borderRadius: '10px',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
|
||||
function FeedbackForm({ t }) {
|
||||
const [isFormSubmitted, setIsFormSubmitted] = useState(false);
|
||||
|
||||
const schema = yup.object().shape({
|
||||
name: yup.string().required(t('feedback_required')),
|
||||
email: yup.string().email(t('feedback_email_error')).required(t('feedback_required')),
|
||||
message: yup
|
||||
.string()
|
||||
.required(t('feedback_required'))
|
||||
.min(5, t('min_length_error', { length: 5 }))
|
||||
.max(255, t('max_length_error', { length: 255 })),
|
||||
});
|
||||
|
||||
const { control, formState, handleSubmit, setError } = useForm({
|
||||
mode: 'onChange',
|
||||
defaultValues,
|
||||
resolver: yupResolver(schema),
|
||||
});
|
||||
|
||||
const { isValid, dirtyFields, errors } = formState;
|
||||
|
||||
function onSubmit() {
|
||||
// setError('root', 'error');
|
||||
setIsFormSubmitted(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<section id="contacts" className="pt-72 mb-88">
|
||||
{!isFormSubmitted && (
|
||||
<form
|
||||
name="signinForm"
|
||||
noValidate
|
||||
className="grid grid-cols-2 gap-x-20 gap-y-32 px-80 py-40 bg-primary-light rounded-20"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<legend className="col-span-2 justify-self-center max-w-[860px] mb-8 text-4xl font-medium text-center">
|
||||
{t('feedback_title')}
|
||||
</legend>
|
||||
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledTextField
|
||||
{...field}
|
||||
label={t('feedback_name')}
|
||||
type="text"
|
||||
error={!!errors.name}
|
||||
helperText={errors?.name?.message}
|
||||
variant="outlined"
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<StyledTextField
|
||||
{...field}
|
||||
label={t('feedback_email')}
|
||||
type="email"
|
||||
error={!!errors.email}
|
||||
helperText={errors?.email?.message}
|
||||
variant="outlined"
|
||||
required
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="message"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
className="col-span-2 mb-8"
|
||||
label={t('feedback_message')}
|
||||
type="text"
|
||||
error={!!errors.message}
|
||||
helperText={errors?.message?.message}
|
||||
variant="outlined"
|
||||
required
|
||||
multiline
|
||||
rows={5}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
sx: {
|
||||
padding: 0,
|
||||
background: (theme) => theme.palette.background.paper,
|
||||
borderRadius: '10px',
|
||||
},
|
||||
}}
|
||||
// eslint-disable-next-line react/jsx-no-duplicate-props
|
||||
inputProps={{
|
||||
sx: {
|
||||
padding: '16.5px 14px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="col-span-2 justify-self-center flex flex-col items-center justify-center gap-10 w-full">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className="w-[220px] text-base uppercase rounded-xl"
|
||||
aria-label={t('feedback_btn')}
|
||||
disabled={_.isEmpty(dirtyFields) || !isValid}
|
||||
type="submit"
|
||||
size="large"
|
||||
>
|
||||
{t('feedback_btn')}
|
||||
</Button>
|
||||
{errors.root?.message && (
|
||||
<p className="text-l text-error-main">{errors.root?.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTranslation('homePage')(memo(FeedbackForm));
|
||||
18
src/app/main/home/components/Statistics.js
Normal file
18
src/app/main/home/components/Statistics.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { memo } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import StatisticsCard from './StatisticsCard';
|
||||
|
||||
function Statistics({ t }) {
|
||||
return (
|
||||
<section className="flex flex-col items-center">
|
||||
<div className="flex gap-20 mb-[136px]">
|
||||
<StatisticsCard title={t('stat_title_1')} text={t('stat_text_1')} />
|
||||
<StatisticsCard title={t('stat_title_2')} text={t('stat_text_2')} />
|
||||
<StatisticsCard title={t('stat_title_3')} text={t('stat_text_3')} />
|
||||
</div>
|
||||
<h2 className="max-w-[1020px] mb-4 text-5xl text-center">{t('caption')}</h2>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTranslation('homePage')(memo(Statistics));
|
||||
12
src/app/main/home/components/StatisticsCard.js
Normal file
12
src/app/main/home/components/StatisticsCard.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
function StatisticsCard({ title, text }) {
|
||||
return (
|
||||
<article className="flex flex-col justify-start items-center max-w-[460px] w-full min-h-[356px] h-full pt-32 px-40 text-common-primary bg-primary-light rounded-20 shadow-light even:bg-secondary-main even:text-primary-light">
|
||||
<h3 className="mb-52 text-[80px] font-semibold">{title}</h3>
|
||||
<p className="text-xl leading-5 font-light">{text}</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(StatisticsCard);
|
||||
48
src/app/main/home/components/Welcome.js
Normal file
48
src/app/main/home/components/Welcome.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import SearchInput from '../../shared-components/SearchInput';
|
||||
|
||||
function Welcome({ t }) {
|
||||
const [query, setQuery] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onInputType = (event) => {
|
||||
const { target } = event;
|
||||
const value = target?.value ?? '';
|
||||
setQuery(value);
|
||||
};
|
||||
|
||||
const onSearch = () => {
|
||||
const trimmedQuery = query.trim();
|
||||
if (!trimmedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/rent-and-buy/search?query=${trimmedQuery}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="h-[calc(100vh-72px)] mb-[105px] bg-home-welcome bg-cover">
|
||||
<div className="flex flex-col max-w-8xl w-full mx-auto pt-160 px-10">
|
||||
<h1 className="max-w-[910px] mb-[30px] text-7xl font-bold text-primary-light">
|
||||
{t('title')}
|
||||
</h1>
|
||||
<p className="max-w-[820px] mb-[100px] text-2xl font-medium text-primary-light">
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
<SearchInput
|
||||
className="max-w-[780px]"
|
||||
mode="simple"
|
||||
placeholder={t('main_input_placeholder')}
|
||||
btnText={t('main_input_btn')}
|
||||
query={query}
|
||||
onType={onInputType}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTranslation('homePage')(memo(Welcome));
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const locale = {};
|
||||
const locale = {
|
||||
search_input_btn: 'calculate',
|
||||
};
|
||||
|
||||
export default locale;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
*/
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import Typography from '@mui/material/Typography';
|
||||
import clsx from 'clsx';
|
||||
import { memo } from 'react';
|
||||
|
||||
function DashboardCategory({ className, title, value, valueColor }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
'flex flex-col items-center justify-center gap-24 max-w-224 w-full h-160 p-24 bg-white shadow-light rounded-20',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Typography variant="body1" className="text-lg text-common-layout font-medium">
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className={clsx('text-5xl font-bold', valueColor && `text-${valueColor}`)}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DashboardCategory);
|
||||
17
src/app/main/navigationPages/shared-components/DateMark.js
Normal file
17
src/app/main/navigationPages/shared-components/DateMark.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import clsx from 'clsx';
|
||||
import { memo } from 'react';
|
||||
|
||||
function DateMark({ className, update }) {
|
||||
return (
|
||||
<span className={clsx('flex justify-center items-center gap-10', className)}>
|
||||
<FuseSvgIcon>heroicons-outline:calendar</FuseSvgIcon>
|
||||
<Typography variant="body1" className="text-lg font-medium text-common-secondary">
|
||||
{update}
|
||||
</Typography>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DateMark);
|
||||
@@ -0,0 +1,21 @@
|
||||
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
|
||||
import clsx from 'clsx';
|
||||
import { memo } from 'react';
|
||||
|
||||
function UpdateMark({ className, favorite, id, onClick }) {
|
||||
const hasCallback = typeof onClick !== 'undefined';
|
||||
|
||||
return (
|
||||
<button
|
||||
className="w-[24px] h-[24px] cursor-pointer"
|
||||
type="button"
|
||||
onClick={() => hasCallback && onClick(id)}
|
||||
>
|
||||
<FuseSvgIcon className={clsx('w-full h-full', className, favorite && 'text-secondary-main')}>
|
||||
heroicons-outline:heart
|
||||
</FuseSvgIcon>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(UpdateMark);
|
||||
25
src/app/main/navigationPages/shared-components/MetaMark.js
Normal file
25
src/app/main/navigationPages/shared-components/MetaMark.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import Typography from '@mui/material/Typography';
|
||||
import _ from '@lodash';
|
||||
import clsx from 'clsx';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
function MetaMark({ className, category, status }) {
|
||||
const text = useMemo(
|
||||
() => (status ? `Status: ${_.startCase(status)}` : _.startCase(category)),
|
||||
[category, status]
|
||||
);
|
||||
|
||||
return (
|
||||
<Typography
|
||||
variant="body1"
|
||||
className={clsx(
|
||||
'flex justify-center align-center px-20 py-2 font-medium border-2 rounded-8',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MetaMark);
|
||||
@@ -0,0 +1,56 @@
|
||||
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import _ from '@lodash';
|
||||
import clsx from 'clsx';
|
||||
import { memo } from 'react';
|
||||
|
||||
function PropertiesHeader({ className, categories, layouts, onCategory, onLayout }) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center gap-44 w-full py-9 px-52 rounded-20 bg-white shadow-light',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="grow flex items-center justify-start gap-16 py-16 border-r-1 border-common-disabled">
|
||||
{categories.map(({ name, amount, active }, idx) => (
|
||||
<button
|
||||
key={name + idx}
|
||||
type="button"
|
||||
className={clsx(
|
||||
'text-2xl text-common-layout cursor-pointer',
|
||||
active && 'text-secondary-main font-semibold cursor-default'
|
||||
)}
|
||||
onClick={() => onCategory(name)}
|
||||
>{`${_.startCase(name)} (${amount})`}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-60 py-16">
|
||||
{layouts.map(({ name, active }, idx) => (
|
||||
<button
|
||||
key={name + idx}
|
||||
type="button"
|
||||
className={clsx(
|
||||
'flex justify-center items-center gap-10 cursor-pointer',
|
||||
active && '!cursor-default'
|
||||
)}
|
||||
onClick={() => onLayout(name)}
|
||||
>
|
||||
<FuseSvgIcon className={clsx('text-common-secondary', active && 'text-secondary-main')}>
|
||||
{`heroicons-outline:view-${name}`}
|
||||
</FuseSvgIcon>
|
||||
<Typography
|
||||
variant="body1"
|
||||
className={clsx('text-2xl text-common-secondary', active && 'text-secondary-main')}
|
||||
>
|
||||
{_.startCase(name)}
|
||||
</Typography>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(PropertiesHeader);
|
||||
@@ -0,0 +1,77 @@
|
||||
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { memo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import DateMark from './DateMark';
|
||||
import FavoriteButton from './FavoriteButton';
|
||||
import MetaMark from './MetaMark';
|
||||
import StatisticsValue from './StatisticsValue';
|
||||
|
||||
function PropertyGridCard({
|
||||
id,
|
||||
image,
|
||||
title,
|
||||
category,
|
||||
status,
|
||||
update,
|
||||
favorite,
|
||||
statistics,
|
||||
onFavorite,
|
||||
onDelete,
|
||||
}) {
|
||||
return (
|
||||
<article className="w-[470px] px-20 pt-20 rounded-20 bg-white shadow-light overflow-hidden">
|
||||
<div className="flex justify-between mb-[25px]">
|
||||
<div className="flex gap-10">
|
||||
<MetaMark
|
||||
category={category}
|
||||
className="text-common-highlight1 border-common-highlight1"
|
||||
/>
|
||||
<MetaMark status={status} className="text-common-highlight2 border-common-highlight2" />
|
||||
</div>
|
||||
<div className="flex gap-20">
|
||||
<FavoriteButton favorite={favorite} id={id} onClick={onFavorite} />
|
||||
<DateMark update={update} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-start mb-[29px]">
|
||||
<Typography variant="h3" className="mb-[17px] text-3xl font-semibold">
|
||||
{title}
|
||||
</Typography>
|
||||
<img src={image} alt={title} className="w-full h-160 rounded-3xl object-cover" />
|
||||
<div className="grid grid-cols-2 justify-between w-full">
|
||||
{statistics.map(({ subject, value, mode }, idx) => (
|
||||
<StatisticsValue
|
||||
key={subject + value + idx}
|
||||
subject={subject}
|
||||
value={value}
|
||||
mode={mode}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-[calc(100%+40px)] -mx-20 border-t-1 border-common-disabled">
|
||||
<button
|
||||
className="flex justify-center items-center gap-10 w-full py-20 border-r-1 border-common-disabled cursor-pointer"
|
||||
type="button"
|
||||
onClick={() => onDelete(id)}
|
||||
>
|
||||
<FuseSvgIcon className="text-common-secondary">heroicons-outline:trash</FuseSvgIcon>
|
||||
<Typography variant="body1" className="text-common-secondary font-medium">
|
||||
Delete
|
||||
</Typography>
|
||||
</button>
|
||||
<Link
|
||||
className="flex justify-center items-center w-full py-[22px] text-lg font-semibold text-secondary-main border-l-1 border-white cursor-pointer"
|
||||
to={`/property/${id}`}
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(PropertyGridCard);
|
||||
@@ -0,0 +1,83 @@
|
||||
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { memo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import DateMark from './DateMark';
|
||||
import FavoriteButton from './FavoriteButton';
|
||||
import MetaMark from './MetaMark';
|
||||
import StatisticsValue from './StatisticsValue';
|
||||
|
||||
function PropertyListItem({
|
||||
id,
|
||||
image,
|
||||
title,
|
||||
category,
|
||||
status,
|
||||
update,
|
||||
favorite,
|
||||
statistics,
|
||||
onFavorite,
|
||||
onDelete,
|
||||
}) {
|
||||
return (
|
||||
<article className="flex w-full p-20 rounded-20 bg-white shadow-light overflow-hidden">
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
className="w-[80px] h-[80px] mr-[15px] rounded-3xl object-cover"
|
||||
/>
|
||||
|
||||
<div className="mr-20">
|
||||
<div className="flex justify-start gap-60 mb-[22px]">
|
||||
<div className="flex gap-10">
|
||||
<MetaMark
|
||||
category={category}
|
||||
className="text-common-highlight1 border-common-highlight1"
|
||||
/>
|
||||
<MetaMark status={status} className="text-common-highlight2 border-common-highlight2" />
|
||||
</div>
|
||||
<div className="flex gap-20">
|
||||
<FavoriteButton favorite={favorite} id={id} onClick={onFavorite} />
|
||||
<DateMark update={update} />
|
||||
</div>
|
||||
</div>
|
||||
<Typography variant="h3" className="max-w-[480px] text-3xl font-semibold truncate">
|
||||
{title}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div className="grow flex mr-20">
|
||||
{statistics.map(
|
||||
({ subject, value, mode }, idx) =>
|
||||
(idx === 0 || idx === 1) && (
|
||||
<StatisticsValue
|
||||
key={subject + value + idx}
|
||||
subject={subject}
|
||||
value={value}
|
||||
mode={mode}
|
||||
className="-my-[6px]"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-between gap-16">
|
||||
<button
|
||||
className="self-end flex justify-center items-center gap-10 cursor-pointer"
|
||||
type="button"
|
||||
onClick={() => onDelete(id)}
|
||||
>
|
||||
<FuseSvgIcon className="text-common-secondary">heroicons-outline:trash</FuseSvgIcon>
|
||||
</button>
|
||||
<Link
|
||||
className="flex justify-center items-center px-[53px] py-20 -mr-20 -mb-20 text-lg font-semibold text-secondary-main border-l-1 border-t-1 rounded-tl-[20px] border-common-disabled cursor-pointer"
|
||||
to={`/property/${id}`}
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(PropertyListItem);
|
||||
@@ -0,0 +1,38 @@
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { STATISTICS_MODES } from 'app/configs/consts';
|
||||
import clsx from 'clsx';
|
||||
import { memo } from 'react';
|
||||
|
||||
function StatisticsValue({ className, subject, value, mode }) {
|
||||
const isPositive = mode === STATISTICS_MODES.positive;
|
||||
const isExtraPositive = mode === STATISTICS_MODES.extra_positive;
|
||||
const isNegative = mode === STATISTICS_MODES.negative;
|
||||
const isExtraNegative = mode === STATISTICS_MODES.extra_negative;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clsx(
|
||||
'max-w-[210px] w-full py-[15px] pl-20 text-left rounded-xl',
|
||||
className,
|
||||
isExtraPositive && 'bg-accept-light',
|
||||
isExtraNegative && 'bg-error-light'
|
||||
)}
|
||||
>
|
||||
<Typography variant="body1" className="text-lg text-left leading-tight">
|
||||
{subject}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h4"
|
||||
className={clsx(
|
||||
'text-[28px] font-semibold text-left leading-tight',
|
||||
(isPositive || isExtraPositive) && 'text-accept-main',
|
||||
(isNegative || isExtraNegative) && 'text-error-main'
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(StatisticsValue);
|
||||
16
src/app/main/rentAndBuy/RentAndBuy.js
Normal file
16
src/app/main/rentAndBuy/RentAndBuy.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
function RentAndBuy({ t }) {
|
||||
return (
|
||||
<div className="flex flex-col max-w-8xl w-full px-10 py-32 mx-auto">
|
||||
<Typography variant="h1" className="mb-44 text-4xl text-common-layout font-medium">
|
||||
{t('title')}
|
||||
</Typography>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTranslation('rentAndBuyPage')(RentAndBuy);
|
||||
38
src/app/main/rentAndBuy/RentAndBuyConfig.js
Normal file
38
src/app/main/rentAndBuy/RentAndBuyConfig.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { lazy } from 'react';
|
||||
import i18next from 'i18next';
|
||||
|
||||
import RentAndBuy from './RentAndBuy';
|
||||
import en from './i18n/en';
|
||||
|
||||
i18next.addResourceBundle('en', 'rentAndBuyPage', en);
|
||||
|
||||
const SearchAddress = lazy(() => import('./components/SearchAddress/SearchAddress'));
|
||||
const PropertyPreview = lazy(() => import('./components/PropertyPreview/PropertyPreview'));
|
||||
|
||||
const RentAndBuyConfig = {
|
||||
settings: {
|
||||
layout: {
|
||||
config: {},
|
||||
style: 'layout2',
|
||||
},
|
||||
},
|
||||
auth: null,
|
||||
routes: [
|
||||
{
|
||||
path: '/rent-and-buy',
|
||||
element: <RentAndBuy />,
|
||||
children: [
|
||||
{
|
||||
path: 'search',
|
||||
element: <SearchAddress />,
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
element: <PropertyPreview />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default RentAndBuyConfig;
|
||||
@@ -0,0 +1,32 @@
|
||||
import Button from '@mui/material/Button';
|
||||
import { useState } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import RegistrationPopup from 'src/app/main/shared-components/popups/RegistrationPopup';
|
||||
|
||||
function PropertyPreview({ t }) {
|
||||
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||
|
||||
const openPopup = () => setIsPopupOpen(true);
|
||||
const closePopup = () => setIsPopupOpen(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-center">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className="w-384 p-20 text-2xl leading-none rounded-lg"
|
||||
aria-label={t('see_more_btn')}
|
||||
type="button"
|
||||
size="large"
|
||||
onClick={openPopup}
|
||||
>
|
||||
{t('see_more_btn')}
|
||||
</Button>
|
||||
</div>
|
||||
<RegistrationPopup open={isPopupOpen} onClose={closePopup} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTranslation('rentAndBuyPage')(PropertyPreview);
|
||||
@@ -0,0 +1,18 @@
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
function SearchAddress({ t }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-68">
|
||||
<span>How are you?</span>
|
||||
<Link
|
||||
to="/rent-and-buy/preview"
|
||||
className="inline-block w-[182px] py-[17px] text-center text-base text-primary-light font-semibold tracking-widest uppercase rounded-lg bg-secondary-light shadow hover:shadow-hover hover:shadow-secondary-light ease-in-out duration-300"
|
||||
>
|
||||
{t('show_btn')}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTranslation('rentAndBuyPage')(SearchAddress);
|
||||
8
src/app/main/rentAndBuy/i18n/en.js
Normal file
8
src/app/main/rentAndBuy/i18n/en.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const locale = {
|
||||
title: 'Rent&Buy Analysis',
|
||||
show_btn: 'show',
|
||||
see_more_btn: 'See more',
|
||||
view: 'View the Calculation',
|
||||
};
|
||||
|
||||
export default locale;
|
||||
11
src/app/main/shared-components/PropertyAnalysisHeader.js
Normal file
11
src/app/main/shared-components/PropertyAnalysisHeader.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import Paper from '@mui/material/Paper';
|
||||
|
||||
function PropertyAnalysisHeader() {
|
||||
return (
|
||||
<Paper>
|
||||
<div></div>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export default PropertyAnalysisHeader;
|
||||
66
src/app/main/shared-components/SearchInput.js
Normal file
66
src/app/main/shared-components/SearchInput.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import _ from '@lodash';
|
||||
import Button from '@mui/material/Button';
|
||||
// import Select from '@mui/material/Select';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, memo, useCallback } from 'react';
|
||||
|
||||
const SEARCH_INPUT_MODES = {
|
||||
simple: 'simple',
|
||||
manual: 'manual',
|
||||
};
|
||||
|
||||
const StyledTextField = forwardRef((props, ref) => (
|
||||
<TextField
|
||||
InputProps={{
|
||||
sx: {
|
||||
background: (theme) => theme.palette.background.paper,
|
||||
borderRadius: '15px',
|
||||
borderColor: (theme) => theme.palette.secondary.light,
|
||||
borderWidth: '1px',
|
||||
boxShadow: '1px 1px 4px 0px rgba(0, 0, 0, 0.25)',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
|
||||
function SearchInput({ className, mode, placeholder, btnText, query, onType, onSearch }) {
|
||||
const isSimpleMode = mode === SEARCH_INPUT_MODES.simple;
|
||||
const isManualMode = mode === SEARCH_INPUT_MODES.manual;
|
||||
const hasBtn = isSimpleMode || isManualMode;
|
||||
|
||||
const debouncedOnType = useCallback(_.debounce(onType, 250), [onType]);
|
||||
|
||||
return (
|
||||
<form className={clsx('flex items-center gap-20', className)}>
|
||||
<StyledTextField
|
||||
type="text"
|
||||
variant="outlined"
|
||||
className="w-full bourder-0"
|
||||
defaultValue={query}
|
||||
placeholder={placeholder ?? ''}
|
||||
onChange={debouncedOnType}
|
||||
/>
|
||||
|
||||
{/* {isManualMode && <Select />} */}
|
||||
|
||||
{hasBtn && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="inherit"
|
||||
className="text-center text-base text-primary-light font-semibold tracking-widest uppercase rounded-2xl bg-secondary-light shadow hover:shadow-hover hover:shadow-secondary-light ease-in-out duration-300"
|
||||
aria-label={btnText ?? ''}
|
||||
type="button"
|
||||
size="large"
|
||||
onClick={onSearch}
|
||||
>
|
||||
{btnText}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(SearchInput);
|
||||
44
src/app/main/shared-components/popups/BasicPopup.js
Normal file
44
src/app/main/shared-components/popups/BasicPopup.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import Backdrop from '@mui/material/Backdrop';
|
||||
import Box from '@mui/material/Box';
|
||||
import Fade from '@mui/material/Fade';
|
||||
import Modal from '@mui/material/Modal';
|
||||
import { memo } from 'react';
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 400,
|
||||
bgcolor: 'inherit',
|
||||
boxShadow: 24,
|
||||
overflow: 'hidden',
|
||||
};
|
||||
|
||||
function BasicPopup({ children, className, open, onClose }) {
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
aria-labelledby="transition-modal-title"
|
||||
aria-describedby="transition-modal-description"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
closeAfterTransition
|
||||
slots={{ backdrop: Backdrop }}
|
||||
slotProps={{
|
||||
backdrop: {
|
||||
timeout: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Fade in={open}>
|
||||
<Box sx={style} className={className}>
|
||||
{children}
|
||||
</Box>
|
||||
</Fade>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(BasicPopup);
|
||||
72
src/app/main/shared-components/popups/RegistrationPopup.js
Normal file
72
src/app/main/shared-components/popups/RegistrationPopup.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { memo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import BasicPopup from './BasicPopup';
|
||||
|
||||
const bullets = [
|
||||
'Lorem ipsum rci egestas. Tortor nulla ac est nulla nisl ut.',
|
||||
'Lorem ipsum dolor sit amet consectetur.',
|
||||
'Lorem ipsum rci egestas. Tortor nulla ac est nulla nisl ut.',
|
||||
'Lorem ipsum dolor sit amet consectetur.',
|
||||
'Lorem ipsum dolor sit amet consectetur.',
|
||||
'Lorem ipsum dt amet consectetur. Duis massa vel estas. Tortor nulla ac est nulla nisl ut.',
|
||||
'Lorem ipsum dolor sit amet consectetur.',
|
||||
];
|
||||
|
||||
function RegistrationPopup({ open, onClose }) {
|
||||
return (
|
||||
<BasicPopup
|
||||
className="flex max-w-[76vw] w-full h-[66vh] min-h-[600px] rounded-20"
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Box
|
||||
className="relative hidden md:flex flex-auto items-center justify-center h-full p-64 lg:px-112 overflow-hidden max-w-[45vw] w-full"
|
||||
sx={{ backgroundColor: 'common.layout' }}
|
||||
>
|
||||
<svg
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
viewBox="0 0 960 540"
|
||||
width="100%"
|
||||
height="100%"
|
||||
preserveAspectRatio="xMidYMax slice"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<Box
|
||||
component="g"
|
||||
sx={{ color: 'primary.light' }}
|
||||
className="opacity-20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="100"
|
||||
>
|
||||
<circle r="234" cx="266" cy="23" />
|
||||
<circle r="234" cx="790" cy="551" />
|
||||
</Box>
|
||||
</svg>
|
||||
</Box>
|
||||
<Box
|
||||
className="flex flex-col items-center px-60 pt-56"
|
||||
sx={{ backgroundColor: 'background.paper' }}
|
||||
>
|
||||
<Typography variant="h4" className="mb-52 text-4xl font-semibold">
|
||||
Lorem ipsum dolor sit amet consetur
|
||||
</Typography>
|
||||
<ul className="flex flex-col gap-10 mb-68">
|
||||
{bullets.map((bullet) => (
|
||||
<li className="bullet">{bullet}</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link
|
||||
className="w-full py-20 text-center text-xl text-primary-light font-semibold tracking-widest rounded-2xl bg-secondary-main shadow hover:shadow-hover hover:shadow-secondary-main ease-in-out duration-300"
|
||||
to="/sign-up"
|
||||
>
|
||||
Try for free
|
||||
</Link>
|
||||
</Box>
|
||||
</BasicPopup>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(RegistrationPopup);
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
57
src/app/services/propertyService.js
Normal file
57
src/app/services/propertyService.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const widgets = {
|
||||
absorptionRate: 'absorption-rate',
|
||||
crime: 'crime',
|
||||
hazards: 'hazards',
|
||||
mapPreview: 'map-preview',
|
||||
marketTrends: 'market-trends',
|
||||
noise: 'noise',
|
||||
population: 'population',
|
||||
propertyTypes: 'property-types',
|
||||
rentEstimate: 'rent-estimate',
|
||||
thEstimate: 'th-estimate',
|
||||
turnover: 'turnover',
|
||||
walkability: 'walkability',
|
||||
zipCodeMap: 'zip-code-map',
|
||||
};
|
||||
|
||||
export default class PropertyService {
|
||||
#dataApi;
|
||||
|
||||
#widgetApi;
|
||||
|
||||
constructor(config) {
|
||||
const { dataBaseURL, widgetBaseURL, apiKey } = config;
|
||||
this.#dataApi = axios.create({
|
||||
baseURL: dataBaseURL,
|
||||
headers: { 'X-API-Key': apiKey },
|
||||
});
|
||||
this.#widgetApi = axios.create({
|
||||
baseURL: widgetBaseURL,
|
||||
params: {
|
||||
sid: apiKey,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fetchProperty(params) {
|
||||
return this.#dataApi.get('/property', { params });
|
||||
}
|
||||
|
||||
search(body) {
|
||||
return this.#dataApi.post('/search', body);
|
||||
}
|
||||
|
||||
fetchComparables(params) {
|
||||
return this.#dataApi.get('/comparables', { params });
|
||||
}
|
||||
|
||||
fetchWidgetByPropertyId(widget, propertyId, params) {
|
||||
return this.#widgetApi.get(`${widget}/${propertyId}`, { params });
|
||||
}
|
||||
|
||||
fetchWidgetByAddress(widget, params) {
|
||||
return this.#widgetApi.get(`${widget}/address`, { params });
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import { convertToRaw, EditorState } from 'draft-js';
|
||||
import { Editor } from 'react-draft-wysiwyg';
|
||||
import draftToHtml from 'draftjs-to-html';
|
||||
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const Root = styled('div')({
|
||||
'& .rdw-dropdown-selectedtext': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'& .rdw-editor-toolbar': {
|
||||
borderWidth: '0 0 1px 0!important',
|
||||
margin: '0!important',
|
||||
},
|
||||
'& .rdw-editor-main': {
|
||||
padding: '8px 12px',
|
||||
height: `${256}px!important`,
|
||||
},
|
||||
});
|
||||
|
||||
const WYSIWYGEditor = forwardRef((props, ref) => {
|
||||
const [editorState, setEditorState] = useState(EditorState.createEmpty());
|
||||
|
||||
function onEditorStateChange(_editorState) {
|
||||
setEditorState(_editorState);
|
||||
|
||||
return props.onChange(draftToHtml(convertToRaw(_editorState.getCurrentContent())));
|
||||
}
|
||||
|
||||
return (
|
||||
<Root className={clsx('rounded-4 border-1 overflow-hidden w-full', props.className)} ref={ref}>
|
||||
<Editor editorState={editorState} onEditorStateChange={onEditorStateChange} />
|
||||
</Root>
|
||||
);
|
||||
});
|
||||
|
||||
export default WYSIWYGEditor;
|
||||
@@ -1,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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ const config = {
|
||||
title: 'Layout 1 - Dashboard',
|
||||
defaults: {
|
||||
mode: 'container',
|
||||
containerWidth: 1570,
|
||||
containerWidth: 1590,
|
||||
navbar: {
|
||||
display: true,
|
||||
folded: true,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { navbarCloseMobile, selectFuseNavbar } from 'app/store/fuse/navbarSlice'
|
||||
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
|
||||
import NavbarLayout1Content from './NavbarLayout1Content';
|
||||
|
||||
const navbarWidth = 280;
|
||||
const navbarWidth = 330;
|
||||
|
||||
const StyledNavBar = styled('div')(({ theme, open, position }) => ({
|
||||
minWidth: navbarWidth,
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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-common-layout">
|
||||
<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>
|
||||
|
||||
@@ -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-primary-light">
|
||||
<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
|
||||
@@ -19,15 +19,7 @@ function HeaderLayout2(props) {
|
||||
</Link>
|
||||
|
||||
<nav className="flex grow justify-center gap-72 items-center">
|
||||
{linksConfigLayout2.map((path) => (
|
||||
<Link
|
||||
className="text-lg leading-5 text-common-layout no-underline"
|
||||
to={`/${path}`}
|
||||
key={path}
|
||||
>
|
||||
{t(path)}
|
||||
</Link>
|
||||
))}
|
||||
<Links className="text-lg leading-5 text-common-layout no-underline" />
|
||||
</nav>
|
||||
|
||||
{props.isAuthenticated || (
|
||||
|
||||
26
src/app/theme-layouts/layout2/components/NavLinks.js
Normal file
26
src/app/theme-layouts/layout2/components/NavLinks.js
Normal 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);
|
||||
@@ -1,3 +0,0 @@
|
||||
const linksConfigLayout2 = ['rent-and-buy', 'about-us', 'blog', 'contacts'];
|
||||
|
||||
export default linksConfigLayout2;
|
||||
@@ -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
7
src/app/utils/index.js
Normal 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;
|
||||
});
|
||||
BIN
src/assets/images/welcome-background.webp
Normal file
BIN
src/assets/images/welcome-background.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 MiB |
@@ -1,5 +0,0 @@
|
||||
const en = {
|
||||
translation: {},
|
||||
};
|
||||
|
||||
export default en;
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* Custom base styles
|
||||
*/
|
||||
@@ -25,17 +24,19 @@ html {
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
line-height: normal;
|
||||
overflow-x: hidden;
|
||||
font-feature-settings: "salt";
|
||||
font-feature-settings: 'salt';
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
-webkit-font-smoothing: auto;
|
||||
-moz-osx-font-smoothing: auto;
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
@@ -47,33 +48,42 @@ html, body {
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/* layout 2 workaround */
|
||||
#root:has(> #fuse-main) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
h1, .h1 {
|
||||
h1,
|
||||
.h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h2, .h2 {
|
||||
h2,
|
||||
.h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h3, .h3 {
|
||||
h3,
|
||||
.h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
h4, .h4 {
|
||||
h4,
|
||||
.h4 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
h5, .h5 {
|
||||
h5,
|
||||
.h5 {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
h6, .h6 {
|
||||
h6,
|
||||
.h6 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -82,11 +92,11 @@ h6, .h6 {
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
a[role=button] {
|
||||
a[role='button'] {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[role="tooltip"] {
|
||||
[role='tooltip'] {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
@@ -137,10 +147,9 @@ button:focus {
|
||||
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 {
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@
|
||||
|
||||
@media print {
|
||||
/* html and body tweaks */
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
height: auto !important;
|
||||
overflow: initial !important;
|
||||
background: none
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* Page breaks */
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
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-family: Roboto Mono, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 4;
|
||||
@@ -18,32 +18,31 @@ pre[class*="language-"] {
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
|
||||
code[class*="language-"]::-moz-selection,
|
||||
pre[class*="language-"]::-moz-selection,
|
||||
code[class*="language-"] ::-moz-selection,
|
||||
pre[class*="language-"] ::-moz-selection {
|
||||
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 {
|
||||
code[class*='language-']::selection,
|
||||
pre[class*='language-']::selection,
|
||||
code[class*='language-'] ::selection,
|
||||
pre[class*='language-'] ::selection {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"] {
|
||||
:not(pre) > code[class*='language-'] {
|
||||
white-space: normal;
|
||||
border-radius: 0.2em;
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
pre[class*="language-"] {
|
||||
pre[class*='language-'] {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
border-radius: 4px;;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.language-css > code,
|
||||
@@ -52,7 +51,7 @@ pre[class*="language-"] {
|
||||
color: #fd9170;
|
||||
}
|
||||
|
||||
[class*="language-"] .namespace {
|
||||
[class*='language-'] .namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,10 +60,10 @@ table.simple.borderless {
|
||||
border: none;
|
||||
}
|
||||
|
||||
table.simple.borderless tbody tr td{
|
||||
table.simple.borderless tbody tr td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
table.simple.borderless thead tr th{
|
||||
table.simple.borderless thead tr th {
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -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,7 @@ 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)',
|
||||
@@ -184,6 +186,8 @@ module.exports = {
|
||||
primary: '#151B30',
|
||||
secondary: '#6D6D6D',
|
||||
disabled: '#D9D9D9',
|
||||
highlight1: '#FFBC6E',
|
||||
highlight2: '#DB00FF',
|
||||
},
|
||||
primary: {
|
||||
light: '#FFFFFF',
|
||||
@@ -1258,6 +1262,7 @@ module.exports = {
|
||||
'5xl': '102.4rem',
|
||||
'6xl': '115.2rem',
|
||||
'7xl': '128rem',
|
||||
'8xl': '142rem',
|
||||
px: '1px',
|
||||
0: '0px',
|
||||
0.5: '0.05rem',
|
||||
|
||||
Reference in New Issue
Block a user