Compare commits
22 Commits
2e3adc86a9
...
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 |
@@ -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) => {
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export 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;
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -9,7 +9,7 @@ function AboutUs({ t }) {
|
||||
<div className="flex gap-64 mb-[126px]">
|
||||
<div className="flex items-center">
|
||||
<iframe
|
||||
className="rounded-[20px]"
|
||||
className="rounded-20"
|
||||
width="715"
|
||||
height="402"
|
||||
src="https://www.youtube.com/embed/rNSIwjmynYQ?controls=0"
|
||||
@@ -18,7 +18,7 @@ function AboutUs({ t }) {
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
<aside className="flex flex-col items-center py-40 px-52 bg-primary-light rounded-[20px]">
|
||||
<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>
|
||||
@@ -26,7 +26,7 @@ function AboutUs({ t }) {
|
||||
</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"
|
||||
to="/rent-and-buy/search"
|
||||
>
|
||||
{t('research_btn')}
|
||||
</Link>
|
||||
|
||||
@@ -4,10 +4,10 @@ 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-[20px] shadow-light">
|
||||
<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-[20px] object-cover"
|
||||
className="w-full h-[230px] mb-20 rounded-20 object-cover"
|
||||
src={image}
|
||||
alt={title}
|
||||
width="460"
|
||||
|
||||
@@ -58,7 +58,7 @@ function FeedbackForm({ t }) {
|
||||
<form
|
||||
name="signinForm"
|
||||
noValidate
|
||||
className="grid grid-cols-2 gap-x-20 gap-y-32 px-80 py-40 bg-primary-light rounded-[20px]"
|
||||
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">
|
||||
|
||||
@@ -2,7 +2,7 @@ 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-[20px] shadow-light even:bg-secondary-main even:text-primary-light">
|
||||
<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>
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
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 ?? '';
|
||||
console.log(value);
|
||||
setQuery(value);
|
||||
};
|
||||
|
||||
const onSearch = () => {
|
||||
// query
|
||||
const trimmedQuery = query.trim();
|
||||
if (!trimmedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(`/rent-and-buy/search?query=${trimmedQuery}`);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -26,6 +32,7 @@ function Welcome({ t }) {
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
<SearchInput
|
||||
className="max-w-[780px]"
|
||||
mode="simple"
|
||||
placeholder={t('main_input_placeholder')}
|
||||
btnText={t('main_input_btn')}
|
||||
|
||||
@@ -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;
|
||||
*/
|
||||
|
||||
@@ -104,6 +104,7 @@ function ProfilePage({ t }) {
|
||||
});
|
||||
const { dirtyFields, errors } = formState;
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
const uploadPicture = async (event) => {
|
||||
const { target } = event;
|
||||
if (target.files && target.files[0]) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { lazy } from 'react';
|
||||
import i18next from 'i18next';
|
||||
|
||||
import authRoles from '../../../configs/authRoles';
|
||||
import { authRoles } from '../../../configs/consts';
|
||||
import en from './i18n/en';
|
||||
|
||||
i18next.addResourceBundle('en', 'profilePage', en);
|
||||
|
||||
@@ -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;
|
||||
@@ -1,7 +1,14 @@
|
||||
import { forwardRef, memo, useCallback } from 'react';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Button from '@mui/material/Button';
|
||||
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
|
||||
@@ -19,28 +26,32 @@ const StyledTextField = forwardRef((props, ref) => (
|
||||
/>
|
||||
));
|
||||
|
||||
function SearchInput({ mode, placeholder, btnText, query, onType, onSearch }) {
|
||||
const isSimpleMode = mode === 'simple';
|
||||
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="flex items-center gap-20">
|
||||
<form className={clsx('flex items-center gap-20', className)}>
|
||||
<StyledTextField
|
||||
type="text"
|
||||
variant="outlined"
|
||||
className="max-w-[620px] w-full bourder-0"
|
||||
className="w-full bourder-0"
|
||||
defaultValue={query}
|
||||
placeholder={placeholder}
|
||||
placeholder={placeholder ?? ''}
|
||||
onChange={debouncedOnType}
|
||||
/>
|
||||
|
||||
{isSimpleMode && (
|
||||
{/* {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}
|
||||
aria-label={btnText ?? ''}
|
||||
type="button"
|
||||
size="large"
|
||||
onClick={onSearch}
|
||||
|
||||
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);
|
||||
})
|
||||
|
||||
@@ -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,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,14 +104,8 @@ 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 = {
|
||||
@@ -69,6 +114,8 @@ const initialState = {
|
||||
displayName: 'John Doe',
|
||||
email: 'johndoe@withinpixels.com',
|
||||
},
|
||||
history: [],
|
||||
favorites: [],
|
||||
};
|
||||
|
||||
const userSlice = createSlice({
|
||||
@@ -76,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,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,
|
||||
|
||||
@@ -7,7 +7,7 @@ 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>
|
||||
|
||||
@@ -8,7 +8,7 @@ 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
|
||||
|
||||
@@ -7,7 +7,7 @@ function NavLinks({ className }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link className={className} to="/rent-and-buy">
|
||||
<Link className={className} to="/rent-and-buy/search">
|
||||
{t('rent_and_buy')}
|
||||
</Link>
|
||||
<Link className={className} to={{ hash: 'about-us' }}>
|
||||
|
||||
@@ -2,45 +2,46 @@
|
||||
/* Print
|
||||
/*----------------------------------------------------------------*/
|
||||
@media all {
|
||||
/* Never show page breaks in normal view */
|
||||
.page-break-after,
|
||||
.page-break-before {
|
||||
display: none;
|
||||
}
|
||||
/* Never show page breaks in normal view */
|
||||
.page-break-after,
|
||||
.page-break-before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
/* html and body tweaks */
|
||||
html, body {
|
||||
height: auto !important;
|
||||
overflow: initial !important;
|
||||
background: none
|
||||
}
|
||||
/* html and body tweaks */
|
||||
html,
|
||||
body {
|
||||
height: auto !important;
|
||||
overflow: initial !important;
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* Page breaks */
|
||||
.page-break-after {
|
||||
display: block;
|
||||
page-break-after: always;
|
||||
position: relative;
|
||||
}
|
||||
/* Page breaks */
|
||||
.page-break-after {
|
||||
display: block;
|
||||
page-break-after: always;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-break-before {
|
||||
display: block;
|
||||
page-break-before: always;
|
||||
position: relative;
|
||||
}
|
||||
.page-break-before {
|
||||
display: block;
|
||||
page-break-before: always;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* General styles */
|
||||
#fuse-toolbar,
|
||||
#fuse-footer,
|
||||
#fuse-navbar,
|
||||
#fuse-settings-presets,
|
||||
#fuse-layout .ps > .ps__rail-x,
|
||||
#fuse-layout .ps > .ps__rail-y {
|
||||
display: none !important;
|
||||
}
|
||||
/* General styles */
|
||||
#fuse-toolbar,
|
||||
#fuse-footer,
|
||||
#fuse-navbar,
|
||||
#fuse-settings-presets,
|
||||
#fuse-layout .ps > .ps__rail-x,
|
||||
#fuse-layout .ps > .ps__rail-y {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#fuse-layout .ps {
|
||||
overflow: visible !important;
|
||||
}
|
||||
#fuse-layout .ps {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,212 +1,211 @@
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
color: #c3cee3;
|
||||
background: #263238;
|
||||
font-family: Roboto Mono,"Liberation Mono",Menlo,Courier,monospace;
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
code[class*='language-'],
|
||||
pre[class*='language-'] {
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
color: #c3cee3;
|
||||
background: #263238;
|
||||
font-family: Roboto Mono, 'Liberation Mono', Menlo, Courier, monospace;
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 4;
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
|
||||
code[class*="language-"]::-moz-selection,
|
||||
pre[class*="language-"]::-moz-selection,
|
||||
code[class*="language-"] ::-moz-selection,
|
||||
pre[class*="language-"] ::-moz-selection {
|
||||
background: #000000;
|
||||
code[class*='language-']::-moz-selection,
|
||||
pre[class*='language-']::-moz-selection,
|
||||
code[class*='language-'] ::-moz-selection,
|
||||
pre[class*='language-'] ::-moz-selection {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
code[class*="language-"]::selection,
|
||||
pre[class*="language-"]::selection,
|
||||
code[class*="language-"] ::selection,
|
||||
pre[class*="language-"] ::selection {
|
||||
background: #000000;
|
||||
code[class*='language-']::selection,
|
||||
pre[class*='language-']::selection,
|
||||
code[class*='language-'] ::selection,
|
||||
pre[class*='language-'] ::selection {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
:not(pre) > code[class*="language-"] {
|
||||
white-space: normal;
|
||||
border-radius: 0.2em;
|
||||
padding: 0.1em;
|
||||
:not(pre) > code[class*='language-'] {
|
||||
white-space: normal;
|
||||
border-radius: 0.2em;
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
pre[class*="language-"] {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
border-radius: 4px;;
|
||||
pre[class*='language-'] {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.language-css > code,
|
||||
.language-sass > code,
|
||||
.language-scss > code {
|
||||
color: #fd9170;
|
||||
color: #fd9170;
|
||||
}
|
||||
|
||||
[class*="language-"] .namespace {
|
||||
opacity: 0.7;
|
||||
[class*='language-'] .namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token.plain-text {
|
||||
color: #c3cee3;
|
||||
color: #c3cee3;
|
||||
}
|
||||
|
||||
.token.atrule {
|
||||
color: #c792ea;
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.token.attr-name {
|
||||
color: #ffcb6b;
|
||||
color: #ffcb6b;
|
||||
}
|
||||
|
||||
.token.attr-value {
|
||||
color: #c3e88d;
|
||||
color: #c3e88d;
|
||||
}
|
||||
|
||||
.token.attribute {
|
||||
color: #c3e88d;
|
||||
color: #c3e88d;
|
||||
}
|
||||
|
||||
.token.boolean {
|
||||
color: #c792ea;
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.token.builtin {
|
||||
color: #ffcb6b;
|
||||
color: #ffcb6b;
|
||||
}
|
||||
|
||||
.token.cdata {
|
||||
color: #80cbc4;
|
||||
color: #80cbc4;
|
||||
}
|
||||
|
||||
.token.char {
|
||||
color: #80cbc4;
|
||||
color: #80cbc4;
|
||||
}
|
||||
|
||||
.token.class {
|
||||
color: #ffcb6b;
|
||||
color: #ffcb6b;
|
||||
}
|
||||
|
||||
.token.class-name {
|
||||
color: #82aaff;
|
||||
color: #82aaff;
|
||||
}
|
||||
|
||||
.token.color {
|
||||
color: #f2ff00;
|
||||
color: #f2ff00;
|
||||
}
|
||||
|
||||
.token.comment {
|
||||
color: #546e7a;
|
||||
color: #546e7a;
|
||||
}
|
||||
|
||||
.token.constant {
|
||||
color: #c792ea;
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.token.deleted {
|
||||
color: #f07178;
|
||||
color: #f07178;
|
||||
}
|
||||
|
||||
.token.doctype {
|
||||
color: #546e7a;
|
||||
color: #546e7a;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
color: #f07178;
|
||||
color: #f07178;
|
||||
}
|
||||
|
||||
.token.function {
|
||||
color: #c792ea;
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.token.hexcode {
|
||||
color: #f2ff00;
|
||||
color: #f2ff00;
|
||||
}
|
||||
|
||||
.token.id {
|
||||
color: #c792ea;
|
||||
font-weight: bold;
|
||||
color: #c792ea;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.important {
|
||||
color: #c792ea;
|
||||
font-weight: bold;
|
||||
color: #c792ea;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.inserted {
|
||||
color: #80cbc4;
|
||||
color: #80cbc4;
|
||||
}
|
||||
|
||||
.token.keyword {
|
||||
color: #c792ea;
|
||||
font-style: italic;
|
||||
color: #c792ea;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #fd9170;
|
||||
color: #fd9170;
|
||||
}
|
||||
|
||||
.token.operator {
|
||||
color: #89ddff;
|
||||
color: #89ddff;
|
||||
}
|
||||
|
||||
.token.prolog {
|
||||
color: #546e7a;
|
||||
color: #546e7a;
|
||||
}
|
||||
|
||||
.token.property {
|
||||
color: #80cbc4;
|
||||
color: #80cbc4;
|
||||
}
|
||||
|
||||
.token.pseudo-class {
|
||||
color: #c3e88d;
|
||||
color: #c3e88d;
|
||||
}
|
||||
|
||||
.token.pseudo-element {
|
||||
color: #c3e88d;
|
||||
color: #c3e88d;
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: #89ddff;
|
||||
color: #89ddff;
|
||||
}
|
||||
|
||||
.token.regex {
|
||||
color: #f2ff00;
|
||||
color: #f2ff00;
|
||||
}
|
||||
|
||||
.token.selector {
|
||||
color: #f07178;
|
||||
color: #f07178;
|
||||
}
|
||||
|
||||
.token.string {
|
||||
color: #c3e88d;
|
||||
color: #c3e88d;
|
||||
}
|
||||
|
||||
.token.symbol {
|
||||
color: #c792ea;
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.token.tag {
|
||||
color: #f07178;
|
||||
color: #f07178;
|
||||
}
|
||||
|
||||
.token.unit {
|
||||
color: #f07178;
|
||||
color: #f07178;
|
||||
}
|
||||
|
||||
.token.url {
|
||||
color: #fd9170;
|
||||
color: #fd9170;
|
||||
}
|
||||
|
||||
.token.variable {
|
||||
color: #f07178;
|
||||
color: #f07178;
|
||||
}
|
||||
|
||||
@@ -2,68 +2,68 @@
|
||||
Basic Table Styles
|
||||
*/
|
||||
.table-responsive {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
}
|
||||
|
||||
table.simple {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-spacing: 0;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-spacing: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table.simple thead tr th {
|
||||
padding: 16px 8px;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
white-space: nowrap;
|
||||
padding: 16px 8px;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table.simple thead tr th:first-child {
|
||||
padding-left: 24px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
table.simple thead tr th:last-child {
|
||||
padding-right: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
table.simple tbody tr td {
|
||||
padding: 12px 8px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
padding: 12px 8px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
table.simple tbody tr td:first-child {
|
||||
padding-left: 24px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
table.simple tbody tr td:last-child {
|
||||
padding-right: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
table.simple tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
table.simple.clickable tbody tr {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
table.simple.clickable tbody tr:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
table.simple.borderless {
|
||||
border: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
table.simple.borderless tbody tr td{
|
||||
border: none;
|
||||
table.simple.borderless tbody tr td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
table.simple.borderless thead tr th{
|
||||
border: none;
|
||||
table.simple.borderless thead tr th {
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -186,6 +186,8 @@ module.exports = {
|
||||
primary: '#151B30',
|
||||
secondary: '#6D6D6D',
|
||||
disabled: '#D9D9D9',
|
||||
highlight1: '#FFBC6E',
|
||||
highlight2: '#DB00FF',
|
||||
},
|
||||
primary: {
|
||||
light: '#FFFFFF',
|
||||
|
||||
Reference in New Issue
Block a user