Compare commits

...

7 Commits

18 changed files with 326 additions and 34 deletions

View File

@@ -6,8 +6,9 @@ import Error404Page from '../main/404/Error404Page';
import navigationPagesConfigs from '../main/navigationPages/navigationPagesConfig';
import authPagesConfigs from '../main/authPages/authPagesConfigs';
import HomeConfig from '../main/home/HomeConfig';
import RentAndBuyConfig from '../main/rentAndBuy/RentAndBuyConfig';
const routeConfigs = [...navigationPagesConfigs, ...authPagesConfigs, HomeConfig];
const routeConfigs = [...navigationPagesConfigs, ...authPagesConfigs, HomeConfig, RentAndBuyConfig];
const routes = [
...FuseUtils.generateRoutesFromConfigs(routeConfigs, settingsConfig.defaultAuth),

View File

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

View File

@@ -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')}

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import FusePageSimple from '@fuse/core/FusePageSimple';
import { Paper } from '@mui/material';
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';

View File

@@ -1,5 +1,5 @@
import FusePageSimple from '@fuse/core/FusePageSimple';
import { Paper } from '@mui/material';
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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
import AuthService from './authService';
import FirebaseService from './firebaseService';
import PropertyService from './propertyService';
// TODO: change to firebase secrets or to use firebase functions
const firebaseConfig = {
apiKey: 'AIzaSyBqMGmOF0-DkYDpnsmZwpf5S8w5cL3fBb8',
@@ -12,7 +14,15 @@ const firebaseConfig = {
measurementId: 'G-JW7J8ZQ9FJ',
};
// TopHap
const propertyConfig = {
dataBaseURL: process.env.REACT_APP_PROPERTY_DATA_BASE_URL,
widgetBaseURL: process.env.REACT_APP_PROPERTY_WIDGET_BASE_URL,
apiKey: process.env.REACT_APP_PROPERTY_API_KEY,
};
const firebase = new FirebaseService(firebaseConfig);
const authService = new AuthService();
const propertyService = new PropertyService(propertyConfig);
export { authService, firebase };
export { authService, firebase, propertyService };

View File

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

View File

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