Compare commits

..

8 Commits

55 changed files with 328 additions and 1400 deletions

View File

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

View File

@@ -1,19 +0,0 @@
export const STATISTICS_MODES = {
positive: 'positive',
extra_positive: 'extra_positive',
negative: 'negative',
extra_negative: 'extra_negative',
};
export const PROPERTIES_LAYOUTS = {
list: 'list',
grid: 'grid',
};
// Authorization Roles
export const authRoles = {
admin: ['admin'],
staff: ['admin', 'staff'],
user: ['admin', 'staff', 'user'],
onlyGuest: [],
};

View File

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

View File

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

View File

@@ -1,3 +1,20 @@
export { default as useWindowDimensions } from './useWindowDimensions';
export { default as useOnClickOutside } from './useOnClickOutside';
export { default as usePropertiesHeader } from './usePropertiesHeader';
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;
}

View File

@@ -1,29 +0,0 @@
import { useEffect } from 'react';
export default function useOnClickOutside(ref, handler) {
useEffect(() => {
const onEvent = (event) => {
if (!ref.current) {
return;
}
if (event instanceof KeyboardEvent && event.key === 'Escape') {
handler(event);
} else if (ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', onEvent);
document.addEventListener('touchstart', onEvent);
document.addEventListener('keydown', onEvent);
return () => {
document.removeEventListener('mousedown', onEvent);
document.removeEventListener('touchstart', onEvent);
document.removeEventListener('keydown', onEvent);
};
}, [ref, handler]);
}

View File

@@ -1,105 +0,0 @@
import { PROPERTIES_LAYOUTS } from 'app/configs/consts';
import { useState, useMemo, useCallback, useEffect } from 'react';
export default function usePropertiesHeader(items) {
const [categories, setCategories] = useState([
{
name: 'all',
amount: 0,
active: true,
},
]);
const [layouts, setLayouts] = useState([
{ name: PROPERTIES_LAYOUTS.list, active: true },
{ name: PROPERTIES_LAYOUTS.grid, active: false },
]);
const activeCategory = useMemo(
() => categories.find(({ active }) => active)?.name ?? categories[0].name,
[categories]
);
const activeLayout = useMemo(
() => layouts.find(({ active }) => active)?.name ?? PROPERTIES_LAYOUTS.list,
[layouts]
);
const onCategory = useCallback(
(value) => {
if (value === activeCategory) {
return;
}
setCategories((prevState) =>
prevState.map((category) => ({ ...category, active: category.name === value }))
);
},
[activeCategory]
);
const onLayout = useCallback(
(value) => {
if (value === activeLayout) {
return;
}
setLayouts((prevState) => prevState.map(({ name }) => ({ name, active: name === value })));
},
[activeLayout]
);
const onItemDelete = (itemCategory) => {
setCategories((prevState) => {
const isItemCategoryLast =
prevState.find((category) => category.name === itemCategory)?.amount === 1;
return prevState
.map((category, idx) => {
if (!idx) {
return {
name: category.name,
amount: category.amount - 1,
active: isItemCategoryLast,
};
}
if (category?.name === itemCategory) {
if (category.amount > 1) {
return { ...category, amount: category.amount - 1 };
}
return null;
}
return category;
})
.filter((category) => category);
});
};
useEffect(() => {
let updatedCategories = [...categories];
items.forEach((item) => {
const hasItemCategory = updatedCategories.find((category) => category.name === item.category);
updatedCategories = updatedCategories.map((category, idx) => {
if (!idx) {
category.amount += 1;
}
return category;
});
if (hasItemCategory) {
updatedCategories = updatedCategories.map((category) => ({
...category,
amount: item.category === category.name ? category.amount + 1 : category.amount,
}));
} else {
updatedCategories.push({ name: item.category, amount: 1, active: false });
}
});
setCategories(updatedCategories);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return { categories, activeCategory, onCategory, layouts, activeLayout, onLayout, onItemDelete };
}

View File

@@ -1,20 +0,0 @@
import { useState, useEffect } from 'react';
export default function useWindowDimensions() {
const getWindowDimensions = () => {
const { innerWidth: width, innerHeight: height } = window;
return { width, height };
};
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
useEffect(() => {
const onRecize = () => setWindowDimensions(getWindowDimensions());
window.addEventListener('resize', onRecize);
return () => window.removeEventListener('resize', onRecize);
}, []);
return windowDimensions;
}

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ function AboutUs({ t }) {
<div className="flex gap-64 mb-[126px]">
<div className="flex items-center">
<iframe
className="rounded-20"
className="rounded-[20px]"
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-20">
<aside className="flex flex-col items-center py-40 px-52 bg-primary-light rounded-[20px]">
<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/search"
to="/rent-and-buy"
>
{t('research_btn')}
</Link>

View File

@@ -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-20 shadow-light">
<article className="flex flex-col justify-between max-w-[460px] w-full h-[526px] bg-primary-light rounded-[20px] shadow-light">
<div>
<img
className="w-full h-[230px] mb-20 rounded-20 object-cover"
className="w-full h-[230px] mb-20 rounded-[20px] object-cover"
src={image}
alt={title}
width="460"

View File

@@ -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-20"
className="grid grid-cols-2 gap-x-20 gap-y-32 px-80 py-40 bg-primary-light rounded-[20px]"
onSubmit={handleSubmit(onSubmit)}
>
<legend className="col-span-2 justify-self-center max-w-[860px] mb-8 text-4xl font-medium text-center">

View File

@@ -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-20 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-[20px] 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>

View File

@@ -1,25 +1,19 @@
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 = () => {
const trimmedQuery = query.trim();
if (!trimmedQuery) {
return;
}
navigate(`/rent-and-buy/search?query=${trimmedQuery}`);
// query
};
return (
@@ -32,7 +26,6 @@ function Welcome({ t }) {
{t('subtitle')}
</p>
<SearchInput
className="max-w-[780px]"
mode="simple"
placeholder={t('main_input_placeholder')}
btnText={t('main_input_btn')}

View File

@@ -1,43 +1,7 @@
import FusePageSimple from '@fuse/core/FusePageSimple';
import Paper from '@mui/material/Paper';
import { styled } from '@mui/material/styles';
import { useState } from 'react';
import { withTranslation } from 'react-i18next';
import SearchInput from '../../shared-components/SearchInput';
import DashboardCategory from '../shared-components/DashboardCategory';
const categoriesMock = [
{
title: 'All Properties',
value: 34,
valueColor: 'secondary-main',
},
{
title: 'New',
value: 12,
valueColor: 'common-highlight2',
},
{
title: 'In Research',
value: 3,
valueColor: 'secondary-main',
},
{
title: 'Interested',
value: 25,
valueColor: 'common-highlight1',
},
{
title: 'Purchased',
value: 8,
valueColor: 'accept-main',
},
{
title: 'Not Interested',
value: 11,
valueColor: 'error-main',
},
];
import { useTranslation } from 'react-i18next';
import FusePageSimple from '@fuse/core/FusePageSimple';
import DemoContent from '@fuse/core/DemoContent';
const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-header': {
@@ -52,39 +16,21 @@ const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-sidebarContent': {},
}));
function DashboardPage({ t }) {
const [query, setQuery] = useState('');
const onInputType = (event) => {
const { target } = event;
const value = target?.value ?? '';
setQuery(value);
};
const onSearch = () => {
// query
};
function DashboardPage(props) {
const { t } = useTranslation('dashboardPage');
return (
<Root
// header={
// <div className="p-24">
// <h4>{t('TITLE')}</h4>
// </div>
// }
content={
<div className="w-full p-60">
<div className="flex flex-wrap justify-center items-center gap-20 mb-52">
{categoriesMock.map(({ title, value, valueColor }) => (
<DashboardCategory title={title} value={value} valueColor={valueColor} />
))}
</div>
<SearchInput
className="mb-28"
mode="manual"
btnText={t('search_input_btn')}
query={query}
onType={onInputType}
onSearch={onSearch}
/>
<Paper className="w-full h-640 mb-[30px] rounded-20 shadow-light" />
<div className="p-24">
<h4>Content</h4>
<br />
<DemoContent />
</div>
}
scroll="content"
@@ -92,4 +38,4 @@ function DashboardPage({ t }) {
);
}
export default withTranslation('dashboardPage')(DashboardPage);
export default DashboardPage;

View File

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

View File

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

View File

@@ -1,15 +1,7 @@
import FusePageSimple from '@fuse/core/FusePageSimple';
import Paper from '@mui/material/Paper';
import { styled } from '@mui/material/styles';
import { PROPERTIES_LAYOUTS } from 'app/configs/consts';
import { selectUserFavorites, updateUserFavorites } from 'app/store/userSlice';
import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { usePropertiesHeader } from 'src/app/hooks';
import PropertiesHeader from '../shared-components/PropertiesHeader';
import PropertyGridCard from '../shared-components/PropertyGridCard';
import PropertyListItem from '../shared-components/PropertyListItem';
import { useTranslation } from 'react-i18next';
import FusePageSimple from '@fuse/core/FusePageSimple';
import DemoContent from '@fuse/core/DemoContent';
const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-header': {
@@ -24,73 +16,21 @@ const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-sidebarContent': {},
}));
function FavoritesPage() {
const dispatch = useDispatch();
const items = useSelector(selectUserFavorites);
const { categories, activeCategory, onCategory, layouts, activeLayout, onLayout, onItemDelete } =
usePropertiesHeader(items);
const onFavorite = useCallback(
(id) => {
const targetItem = items.find((item) => item.id === id);
if (!targetItem) {
return;
}
onItemDelete(targetItem?.category);
dispatch(updateUserFavorites(targetItem)).catch((error) => console.log(error));
},
[items, onItemDelete, dispatch]
);
const renderedItems = useMemo(
() =>
items.map((item, idx) => {
if (activeCategory !== 'all' && item.category !== activeCategory) {
return;
}
// eslint-disable-next-line consistent-return
return activeLayout === PROPERTIES_LAYOUTS.list ? (
<PropertyListItem
{...item}
key={item.title + idx}
onDelete={onFavorite}
onFavorite={onFavorite}
/>
) : (
<PropertyGridCard
{...item}
key={item.title + idx}
onDelete={onFavorite}
onFavorite={onFavorite}
/>
);
}),
[items, activeCategory, activeLayout, onFavorite]
);
function FavoritesPage(props) {
const { t } = useTranslation('favoritesPage');
return (
<Root
// header={
// <div className="p-24">
// <h4>{t('TITLE')}</h4>
// </div>
// }
content={
<div className="w-full p-60">
<Paper className="w-full h-320 mb-[30px] rounded-20 shadow-light" />
<PropertiesHeader
className="mb-40"
categories={categories}
layouts={layouts}
onCategory={onCategory}
onLayout={onLayout}
/>
<div
className={clsx(
'w-full flex flex-wrap justify-center gap-28',
activeLayout === PROPERTIES_LAYOUTS.list && 'flex-col'
)}
>
{renderedItems}
</div>
<div className="p-24">
<h4>Content</h4>
<br />
<DemoContent />
</div>
}
scroll="content"

View File

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

View File

@@ -1,15 +1,7 @@
import FusePageSimple from '@fuse/core/FusePageSimple';
import Paper from '@mui/material/Paper';
import { styled } from '@mui/material/styles';
import { PROPERTIES_LAYOUTS } from 'app/configs/consts';
import { selectUserHistory, updateUserFavorites, updateUserHistory } from 'app/store/userSlice';
import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { usePropertiesHeader } from 'src/app/hooks';
import PropertiesHeader from '../shared-components/PropertiesHeader';
import PropertyGridCard from '../shared-components/PropertyGridCard';
import PropertyListItem from '../shared-components/PropertyListItem';
import { useTranslation } from 'react-i18next';
import FusePageSimple from '@fuse/core/FusePageSimple';
import DemoContent from '@fuse/core/DemoContent';
const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-header': {
@@ -24,83 +16,21 @@ const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-sidebarContent': {},
}));
function HistoryPage() {
const dispatch = useDispatch();
const items = useSelector(selectUserHistory);
const { categories, activeCategory, onCategory, layouts, activeLayout, onLayout, onItemDelete } =
usePropertiesHeader(items);
const onDelete = useCallback(
(id) => {
const targetItem = items.find((item) => item.id === id);
const newHistory = items.filter((item) => item.id !== id);
onItemDelete(targetItem?.category);
dispatch(updateUserHistory(newHistory));
},
[items, onItemDelete, dispatch]
);
const onFavorite = useCallback(
(id) => {
const targetItem = items.find((item) => item.id === id);
if (!targetItem) {
return;
}
dispatch(updateUserFavorites(targetItem)).catch((error) => console.log(error));
},
[items, dispatch]
);
const renderedItems = useMemo(
() =>
items.map((item, idx) => {
if (activeCategory !== 'all' && item.category !== activeCategory) {
return;
}
// eslint-disable-next-line consistent-return
return activeLayout === PROPERTIES_LAYOUTS.list ? (
<PropertyListItem
{...item}
key={item.title + idx}
onDelete={onDelete}
onFavorite={onFavorite}
/>
) : (
<PropertyGridCard
{...item}
key={item.title + idx}
onDelete={onDelete}
onFavorite={onFavorite}
/>
);
}),
[items, activeCategory, activeLayout, onDelete, onFavorite]
);
function HistoryPage(props) {
const { t } = useTranslation('historyPage');
return (
<Root
// header={
// <div className="p-24">
// <h4>{t('TITLE')}</h4>
// </div>
// }
content={
<div className="w-full p-60">
<Paper className="w-full h-320 mb-[30px] rounded-20 shadow-light" />
<PropertiesHeader
className="mb-40"
categories={categories}
layouts={layouts}
onCategory={onCategory}
onLayout={onLayout}
/>
<div
className={clsx(
'w-full flex flex-wrap justify-center gap-28',
activeLayout === PROPERTIES_LAYOUTS.list && 'flex-col'
)}
>
{renderedItems}
</div>
<div className="p-24">
<h4>Content</h4>
<br />
<DemoContent />
</div>
}
scroll="content"

View File

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

View File

@@ -104,7 +104,6 @@ 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]) {

View File

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

View File

@@ -1,27 +0,0 @@
import Typography from '@mui/material/Typography';
import clsx from 'clsx';
import { memo } from 'react';
function DashboardCategory({ className, title, value, valueColor }) {
return (
<button
type="button"
className={clsx(
'flex flex-col items-center justify-center gap-24 max-w-224 w-full h-160 p-24 bg-white shadow-light rounded-20',
className
)}
>
<Typography variant="body1" className="text-lg text-common-layout font-medium">
{title}
</Typography>
<Typography
variant="body2"
className={clsx('text-5xl font-bold', valueColor && `text-${valueColor}`)}
>
{value}
</Typography>
</button>
);
}
export default memo(DashboardCategory);

View File

@@ -1,17 +0,0 @@
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import Typography from '@mui/material/Typography';
import clsx from 'clsx';
import { memo } from 'react';
function DateMark({ className, update }) {
return (
<span className={clsx('flex justify-center items-center gap-10', className)}>
<FuseSvgIcon>heroicons-outline:calendar</FuseSvgIcon>
<Typography variant="body1" className="text-lg font-medium text-common-secondary">
{update}
</Typography>
</span>
);
}
export default memo(DateMark);

View File

@@ -1,21 +0,0 @@
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import clsx from 'clsx';
import { memo } from 'react';
function UpdateMark({ className, favorite, id, onClick }) {
const hasCallback = typeof onClick !== 'undefined';
return (
<button
className="w-[24px] h-[24px] cursor-pointer"
type="button"
onClick={() => hasCallback && onClick(id)}
>
<FuseSvgIcon className={clsx('w-full h-full', className, favorite && 'text-secondary-main')}>
heroicons-outline:heart
</FuseSvgIcon>
</button>
);
}
export default memo(UpdateMark);

View File

@@ -1,25 +0,0 @@
import Typography from '@mui/material/Typography';
import _ from '@lodash';
import clsx from 'clsx';
import { memo, useMemo } from 'react';
function MetaMark({ className, category, status }) {
const text = useMemo(
() => (status ? `Status: ${_.startCase(status)}` : _.startCase(category)),
[category, status]
);
return (
<Typography
variant="body1"
className={clsx(
'flex justify-center align-center px-20 py-2 font-medium border-2 rounded-8',
className
)}
>
{text}
</Typography>
);
}
export default memo(MetaMark);

View File

@@ -1,56 +0,0 @@
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import Typography from '@mui/material/Typography';
import _ from '@lodash';
import clsx from 'clsx';
import { memo } from 'react';
function PropertiesHeader({ className, categories, layouts, onCategory, onLayout }) {
return (
<div
className={clsx(
'flex items-center gap-44 w-full py-9 px-52 rounded-20 bg-white shadow-light',
className
)}
>
<div className="grow flex items-center justify-start gap-16 py-16 border-r-1 border-common-disabled">
{categories.map(({ name, amount, active }, idx) => (
<button
key={name + idx}
type="button"
className={clsx(
'text-2xl text-common-layout cursor-pointer',
active && 'text-secondary-main font-semibold cursor-default'
)}
onClick={() => onCategory(name)}
>{`${_.startCase(name)} (${amount})`}</button>
))}
</div>
<div className="flex items-center gap-60 py-16">
{layouts.map(({ name, active }, idx) => (
<button
key={name + idx}
type="button"
className={clsx(
'flex justify-center items-center gap-10 cursor-pointer',
active && '!cursor-default'
)}
onClick={() => onLayout(name)}
>
<FuseSvgIcon className={clsx('text-common-secondary', active && 'text-secondary-main')}>
{`heroicons-outline:view-${name}`}
</FuseSvgIcon>
<Typography
variant="body1"
className={clsx('text-2xl text-common-secondary', active && 'text-secondary-main')}
>
{_.startCase(name)}
</Typography>
</button>
))}
</div>
</div>
);
}
export default memo(PropertiesHeader);

View File

@@ -1,77 +0,0 @@
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import Typography from '@mui/material/Typography';
import { memo } from 'react';
import { Link } from 'react-router-dom';
import DateMark from './DateMark';
import FavoriteButton from './FavoriteButton';
import MetaMark from './MetaMark';
import StatisticsValue from './StatisticsValue';
function PropertyGridCard({
id,
image,
title,
category,
status,
update,
favorite,
statistics,
onFavorite,
onDelete,
}) {
return (
<article className="w-[470px] px-20 pt-20 rounded-20 bg-white shadow-light overflow-hidden">
<div className="flex justify-between mb-[25px]">
<div className="flex gap-10">
<MetaMark
category={category}
className="text-common-highlight1 border-common-highlight1"
/>
<MetaMark status={status} className="text-common-highlight2 border-common-highlight2" />
</div>
<div className="flex gap-20">
<FavoriteButton favorite={favorite} id={id} onClick={onFavorite} />
<DateMark update={update} />
</div>
</div>
<div className="flex flex-col items-start mb-[29px]">
<Typography variant="h3" className="mb-[17px] text-3xl font-semibold">
{title}
</Typography>
<img src={image} alt={title} className="w-full h-160 rounded-3xl object-cover" />
<div className="grid grid-cols-2 justify-between w-full">
{statistics.map(({ subject, value, mode }, idx) => (
<StatisticsValue
key={subject + value + idx}
subject={subject}
value={value}
mode={mode}
/>
))}
</div>
</div>
<div className="flex w-[calc(100%+40px)] -mx-20 border-t-1 border-common-disabled">
<button
className="flex justify-center items-center gap-10 w-full py-20 border-r-1 border-common-disabled cursor-pointer"
type="button"
onClick={() => onDelete(id)}
>
<FuseSvgIcon className="text-common-secondary">heroicons-outline:trash</FuseSvgIcon>
<Typography variant="body1" className="text-common-secondary font-medium">
Delete
</Typography>
</button>
<Link
className="flex justify-center items-center w-full py-[22px] text-lg font-semibold text-secondary-main border-l-1 border-white cursor-pointer"
to={`/property/${id}`}
>
Details
</Link>
</div>
</article>
);
}
export default memo(PropertyGridCard);

View File

@@ -1,83 +0,0 @@
import FuseSvgIcon from '@fuse/core/FuseSvgIcon';
import Typography from '@mui/material/Typography';
import { memo } from 'react';
import { Link } from 'react-router-dom';
import DateMark from './DateMark';
import FavoriteButton from './FavoriteButton';
import MetaMark from './MetaMark';
import StatisticsValue from './StatisticsValue';
function PropertyListItem({
id,
image,
title,
category,
status,
update,
favorite,
statistics,
onFavorite,
onDelete,
}) {
return (
<article className="flex w-full p-20 rounded-20 bg-white shadow-light overflow-hidden">
<img
src={image}
alt={title}
className="w-[80px] h-[80px] mr-[15px] rounded-3xl object-cover"
/>
<div className="mr-20">
<div className="flex justify-start gap-60 mb-[22px]">
<div className="flex gap-10">
<MetaMark
category={category}
className="text-common-highlight1 border-common-highlight1"
/>
<MetaMark status={status} className="text-common-highlight2 border-common-highlight2" />
</div>
<div className="flex gap-20">
<FavoriteButton favorite={favorite} id={id} onClick={onFavorite} />
<DateMark update={update} />
</div>
</div>
<Typography variant="h3" className="max-w-[480px] text-3xl font-semibold truncate">
{title}
</Typography>
</div>
<div className="grow flex mr-20">
{statistics.map(
({ subject, value, mode }, idx) =>
(idx === 0 || idx === 1) && (
<StatisticsValue
key={subject + value + idx}
subject={subject}
value={value}
mode={mode}
className="-my-[6px]"
/>
)
)}
</div>
<div className="flex flex-col justify-between gap-16">
<button
className="self-end flex justify-center items-center gap-10 cursor-pointer"
type="button"
onClick={() => onDelete(id)}
>
<FuseSvgIcon className="text-common-secondary">heroicons-outline:trash</FuseSvgIcon>
</button>
<Link
className="flex justify-center items-center px-[53px] py-20 -mr-20 -mb-20 text-lg font-semibold text-secondary-main border-l-1 border-t-1 rounded-tl-[20px] border-common-disabled cursor-pointer"
to={`/property/${id}`}
>
Details
</Link>
</div>
</article>
);
}
export default memo(PropertyListItem);

View File

@@ -1,38 +0,0 @@
import Typography from '@mui/material/Typography';
import { STATISTICS_MODES } from 'app/configs/consts';
import clsx from 'clsx';
import { memo } from 'react';
function StatisticsValue({ className, subject, value, mode }) {
const isPositive = mode === STATISTICS_MODES.positive;
const isExtraPositive = mode === STATISTICS_MODES.extra_positive;
const isNegative = mode === STATISTICS_MODES.negative;
const isExtraNegative = mode === STATISTICS_MODES.extra_negative;
return (
<span
className={clsx(
'max-w-[210px] w-full py-[15px] pl-20 text-left rounded-xl',
className,
isExtraPositive && 'bg-accept-light',
isExtraNegative && 'bg-error-light'
)}
>
<Typography variant="body1" className="text-lg text-left leading-tight">
{subject}
</Typography>
<Typography
variant="h4"
className={clsx(
'text-[28px] font-semibold text-left leading-tight',
(isPositive || isExtraPositive) && 'text-accept-main',
(isNegative || isExtraNegative) && 'text-error-main'
)}
>
{value}
</Typography>
</span>
);
}
export default memo(StatisticsValue);

View File

@@ -1,16 +0,0 @@
import Typography from '@mui/material/Typography';
import { withTranslation } from 'react-i18next';
import { Outlet } from 'react-router-dom';
function RentAndBuy({ t }) {
return (
<div className="flex flex-col max-w-8xl w-full px-10 py-32 mx-auto">
<Typography variant="h1" className="mb-44 text-4xl text-common-layout font-medium">
{t('title')}
</Typography>
<Outlet />
</div>
);
}
export default withTranslation('rentAndBuyPage')(RentAndBuy);

View File

@@ -1,38 +0,0 @@
import { lazy } from 'react';
import i18next from 'i18next';
import RentAndBuy from './RentAndBuy';
import en from './i18n/en';
i18next.addResourceBundle('en', 'rentAndBuyPage', en);
const SearchAddress = lazy(() => import('./components/SearchAddress/SearchAddress'));
const PropertyPreview = lazy(() => import('./components/PropertyPreview/PropertyPreview'));
const RentAndBuyConfig = {
settings: {
layout: {
config: {},
style: 'layout2',
},
},
auth: null,
routes: [
{
path: '/rent-and-buy',
element: <RentAndBuy />,
children: [
{
path: 'search',
element: <SearchAddress />,
},
{
path: 'preview',
element: <PropertyPreview />,
},
],
},
],
};
export default RentAndBuyConfig;

View File

@@ -1,32 +0,0 @@
import Button from '@mui/material/Button';
import { useState } from 'react';
import { withTranslation } from 'react-i18next';
import RegistrationPopup from 'src/app/main/shared-components/popups/RegistrationPopup';
function PropertyPreview({ t }) {
const [isPopupOpen, setIsPopupOpen] = useState(false);
const openPopup = () => setIsPopupOpen(true);
const closePopup = () => setIsPopupOpen(false);
return (
<>
<div className="flex flex-col items-center">
<Button
variant="contained"
color="secondary"
className="w-384 p-20 text-2xl leading-none rounded-lg"
aria-label={t('see_more_btn')}
type="button"
size="large"
onClick={openPopup}
>
{t('see_more_btn')}
</Button>
</div>
<RegistrationPopup open={isPopupOpen} onClose={closePopup} />
</>
);
}
export default withTranslation('rentAndBuyPage')(PropertyPreview);

View File

@@ -1,18 +0,0 @@
import { withTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
function SearchAddress({ t }) {
return (
<div className="flex flex-col items-center gap-68">
<span>How are you?</span>
<Link
to="/rent-and-buy/preview"
className="inline-block w-[182px] py-[17px] text-center text-base text-primary-light font-semibold tracking-widest uppercase rounded-lg bg-secondary-light shadow hover:shadow-hover hover:shadow-secondary-light ease-in-out duration-300"
>
{t('show_btn')}
</Link>
</div>
);
}
export default withTranslation('rentAndBuyPage')(SearchAddress);

View File

@@ -1,8 +0,0 @@
const locale = {
title: 'Rent&Buy Analysis',
show_btn: 'show',
see_more_btn: 'See more',
view: 'View the Calculation',
};
export default locale;

View File

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

View File

@@ -1,14 +1,7 @@
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',
};
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';
import _ from '@lodash';
const StyledTextField = forwardRef((props, ref) => (
<TextField
@@ -26,32 +19,28 @@ const StyledTextField = forwardRef((props, 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;
function SearchInput({ mode, placeholder, btnText, query, onType, onSearch }) {
const isSimpleMode = mode === 'simple';
const debouncedOnType = useCallback(_.debounce(onType, 250), [onType]);
return (
<form className={clsx('flex items-center gap-20', className)}>
<form className="flex items-center gap-20">
<StyledTextField
type="text"
variant="outlined"
className="w-full bourder-0"
className="max-w-[620px] w-full bourder-0"
defaultValue={query}
placeholder={placeholder ?? ''}
placeholder={placeholder}
onChange={debouncedOnType}
/>
{/* {isManualMode && <Select />} */}
{hasBtn && (
{isSimpleMode && (
<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}

View File

@@ -1,44 +0,0 @@
import Backdrop from '@mui/material/Backdrop';
import Box from '@mui/material/Box';
import Fade from '@mui/material/Fade';
import Modal from '@mui/material/Modal';
import { memo } from 'react';
const style = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'inherit',
boxShadow: 24,
overflow: 'hidden',
};
function BasicPopup({ children, className, open, onClose }) {
return (
<div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={onClose}
closeAfterTransition
slots={{ backdrop: Backdrop }}
slotProps={{
backdrop: {
timeout: 500,
},
}}
>
<Fade in={open}>
<Box sx={style} className={className}>
{children}
</Box>
</Fade>
</Modal>
</div>
);
}
export default memo(BasicPopup);

View File

@@ -1,72 +0,0 @@
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { memo } from 'react';
import { Link } from 'react-router-dom';
import BasicPopup from './BasicPopup';
const bullets = [
'Lorem ipsum rci egestas. Tortor nulla ac est nulla nisl ut.',
'Lorem ipsum dolor sit amet consectetur.',
'Lorem ipsum rci egestas. Tortor nulla ac est nulla nisl ut.',
'Lorem ipsum dolor sit amet consectetur.',
'Lorem ipsum dolor sit amet consectetur.',
'Lorem ipsum dt amet consectetur. Duis massa vel estas. Tortor nulla ac est nulla nisl ut.',
'Lorem ipsum dolor sit amet consectetur.',
];
function RegistrationPopup({ open, onClose }) {
return (
<BasicPopup
className="flex max-w-[76vw] w-full h-[66vh] min-h-[600px] rounded-20"
open={open}
onClose={onClose}
>
<Box
className="relative hidden md:flex flex-auto items-center justify-center h-full p-64 lg:px-112 overflow-hidden max-w-[45vw] w-full"
sx={{ backgroundColor: 'common.layout' }}
>
<svg
className="absolute inset-0 pointer-events-none"
viewBox="0 0 960 540"
width="100%"
height="100%"
preserveAspectRatio="xMidYMax slice"
xmlns="http://www.w3.org/2000/svg"
>
<Box
component="g"
sx={{ color: 'primary.light' }}
className="opacity-20"
fill="none"
stroke="currentColor"
strokeWidth="100"
>
<circle r="234" cx="266" cy="23" />
<circle r="234" cx="790" cy="551" />
</Box>
</svg>
</Box>
<Box
className="flex flex-col items-center px-60 pt-56"
sx={{ backgroundColor: 'background.paper' }}
>
<Typography variant="h4" className="mb-52 text-4xl font-semibold">
Lorem ipsum dolor sit amet consetur
</Typography>
<ul className="flex flex-col gap-10 mb-68">
{bullets.map((bullet) => (
<li className="bullet">{bullet}</li>
))}
</ul>
<Link
className="w-full py-20 text-center text-xl text-primary-light font-semibold tracking-widest rounded-2xl bg-secondary-main shadow hover:shadow-hover hover:shadow-secondary-main ease-in-out duration-300"
to="/sign-up"
>
Try for free
</Link>
</Box>
</BasicPopup>
);
}
export default memo(RegistrationPopup);

View File

@@ -20,7 +20,7 @@ export default class AuthService extends FuseUtils.EventEmitter {
.then((userCredential) => firebaseAuth.updateProfile(userCredential.user, { displayName }))
.then(() => {
const userRef = firebaseDb.ref(this.#db, `users/${this.#auth.currentUser.uid}`);
const value = { role: 'user', data: { displayName, email }, favorites: [] };
const value = { role: 'user', data: { displayName, email } };
return firebaseDb.set(userRef, value);
})

View File

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

View File

@@ -1,57 +0,0 @@
import axios from 'axios';
const widgets = {
absorptionRate: 'absorption-rate',
crime: 'crime',
hazards: 'hazards',
mapPreview: 'map-preview',
marketTrends: 'market-trends',
noise: 'noise',
population: 'population',
propertyTypes: 'property-types',
rentEstimate: 'rent-estimate',
thEstimate: 'th-estimate',
turnover: 'turnover',
walkability: 'walkability',
zipCodeMap: 'zip-code-map',
};
export default class PropertyService {
#dataApi;
#widgetApi;
constructor(config) {
const { dataBaseURL, widgetBaseURL, apiKey } = config;
this.#dataApi = axios.create({
baseURL: dataBaseURL,
headers: { 'X-API-Key': apiKey },
});
this.#widgetApi = axios.create({
baseURL: widgetBaseURL,
params: {
sid: apiKey,
},
});
}
fetchProperty(params) {
return this.#dataApi.get('/property', { params });
}
search(body) {
return this.#dataApi.post('/search', body);
}
fetchComparables(params) {
return this.#dataApi.get('/comparables', { params });
}
fetchWidgetByPropertyId(widget, propertyId, params) {
return this.#widgetApi.get(`${widget}/${propertyId}`, { params });
}
fetchWidgetByAddress(widget, params) {
return this.#widgetApi.get(`${widget}/address`, { params });
}
}

View File

@@ -1,86 +1,35 @@
import browserHistory from '@history';
import _ from '@lodash';
/* eslint import/no-extraneous-dependencies: off */
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import settingsConfig from 'app/configs/settingsConfig';
import { showMessage } from 'app/store/fuse/messageSlice';
import history from '@history';
import _ from '@lodash';
import { setInitialSettings } from 'app/store/fuse/settingsSlice';
import { showMessage } from 'app/store/fuse/messageSlice';
import settingsConfig from 'app/configs/settingsConfig';
import { authService } from '../services';
export const setUser = createAsyncThunk('user/setUser', async (user) => {
// You can redirect the logged-in user to a specific route depending on his role
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
*/
if (user.loginRedirectUrl) {
settingsConfig.loginRedirectUrl = user.loginRedirectUrl; // for example 'apps/academy'
}
return _.merge({}, initialState, user);
return user;
});
export const updateUserSettings = createAsyncThunk(
'user/updateSettings',
async (settings, { dispatch, getState }) => {
const { user } = getState();
const newUser = _.omit(_.merge({}, user, { data: { ...settings } }), 'history');
const newUser = _.merge({}, user, { data: { ...settings } });
dispatch(updateUserData(newUser))
.then(() => {
dispatch(showMessage({ message: 'User data saved' }));
})
.catch((error) => {
dispatch(showMessage({ message: error.message }));
});
dispatch(updateUserData(newUser));
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();
@@ -89,7 +38,7 @@ export const logoutUser = () => async (dispatch, getState) => {
return null;
}
browserHistory.push({
history.push({
pathname: '/',
});
@@ -104,8 +53,14 @@ export const updateUserData = (user) => async (dispatch, getState) => {
return;
}
// eslint-disable-next-line consistent-return
return authService.updateUserData(user);
authService
.updateUserData(user)
.then(() => {
dispatch(showMessage({ message: 'User data saved' }));
})
.catch((error) => {
dispatch(showMessage({ message: error.message }));
});
};
const initialState = {
@@ -114,8 +69,6 @@ const initialState = {
displayName: 'John Doe',
email: 'johndoe@withinpixels.com',
},
history: [],
favorites: [],
};
const userSlice = createSlice({
@@ -123,21 +76,15 @@ 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, userHistoryUpdated } = userSlice.actions;
export const { userLoggedOut } = userSlice.actions;
export const selectUser = ({ user }) => user;
export const selectUserHistory = ({ user }) => user.history;
export const selectUserFavorites = ({ user }) => user.favorites;
export default userSlice.reducer;

View File

@@ -2,7 +2,7 @@ const config = {
title: 'Layout 1 - Dashboard',
defaults: {
mode: 'container',
containerWidth: 1590,
containerWidth: 1570,
navbar: {
display: true,
folded: true,

View File

@@ -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 = 330;
const navbarWidth = 280;
const StyledNavBar = styled('div')(({ theme, open, position }) => ({
minWidth: navbarWidth,

View File

@@ -7,7 +7,7 @@ function FooterLayout2() {
const { t } = useTranslation('layout2');
return (
<footer className="z-[10000] flex items-center justify-center w-full bg-common-layout">
<footer className="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>

View File

@@ -8,7 +8,7 @@ function HeaderLayout2(props) {
const { t } = useTranslation('layout2');
return (
<header className="fixed z-[10000] flex items-center justify-center w-full h-72 px-10 bg-primary-light">
<header className="fixed z-50 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

View File

@@ -7,7 +7,7 @@ function NavLinks({ className }) {
return (
<>
<Link className={className} to="/rent-and-buy/search">
<Link className={className} to="/rent-and-buy">
{t('rent_and_buy')}
</Link>
<Link className={className} to={{ hash: 'about-us' }}>

View File

@@ -11,11 +11,10 @@
@media print {
/* html and body tweaks */
html,
body {
html, body {
height: auto !important;
overflow: initial !important;
background: none;
background: none
}
/* Page breaks */

View File

@@ -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,31 +18,32 @@ 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,
@@ -51,7 +52,7 @@ pre[class*='language-'] {
color: #fd9170;
}
[class*='language-'] .namespace {
[class*="language-"] .namespace {
opacity: 0.7;
}

View File

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

View File

@@ -186,8 +186,6 @@ module.exports = {
primary: '#151B30',
secondary: '#6D6D6D',
disabled: '#D9D9D9',
highlight1: '#FFBC6E',
highlight2: '#DB00FF',
},
primary: {
light: '#FFFFFF',