Merge pull request 'RC-10-favorites-and-history-cards' (#6) from RC-10-favorites-and-history-cards into dev

Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
2023-08-27 14:02:14 +03:00
36 changed files with 958 additions and 294 deletions

View File

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

19
src/app/configs/consts.js Normal file
View File

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

View File

@@ -6,6 +6,63 @@ import { showMessage } from 'app/store/fuse/messageSlice';
import { logoutUser, setUser } from 'app/store/userSlice'; import { logoutUser, setUser } from 'app/store/userSlice';
import { authService, firebase } from '../services'; 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(); const AuthContext = React.createContext();
function AuthProvider({ children }) { function AuthProvider({ children }) {
@@ -25,14 +82,19 @@ function AuthProvider({ children }) {
authService.onAuthStateChanged((authUser) => { authService.onAuthStateChanged((authUser) => {
dispatch(showMessage({ message: 'Signing...' })); dispatch(showMessage({ message: 'Signing...' }));
if (authUser) { if (authUser) {
const storageUser = JSON.parse(localStorage.user ?? '{}');
authService authService
.getUserData(authUser.uid) .getUserData(authUser.uid)
.then((user) => { .then((user) => {
if (user) { if (user) {
success(user, 'Signed in'); success({ ...user, ...storageUser }, 'Signed in');
} else { } else {
// First login
const { displayName, photoURL, email } = authUser; 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) => { .catch((error) => {

View File

@@ -1,20 +1,3 @@
import { useState, useEffect } from 'react'; export { default as useWindowDimensions } from './useWindowDimensions';
export { default as useOnClickOutside } from './useOnClickOutside';
export function useWindowDimensions() { export { default as usePropertiesHeader } from './usePropertiesHeader';
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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import i18next from 'i18next'; import i18next from 'i18next';
import SignUpPage from './SignUpPage'; import SignUpPage from './SignUpPage';
import authRoles from '../../../configs/authRoles'; import authRoles from '../../../configs/consts';
import en from './i18n/en'; import en from './i18n/en';
i18next.addResourceBundle('en', 'signUpPage', 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 gap-64 mb-[126px]">
<div className="flex items-center"> <div className="flex items-center">
<iframe <iframe
className="rounded-[20px]" className="rounded-20"
width="715" width="715"
height="402" height="402"
src="https://www.youtube.com/embed/rNSIwjmynYQ?controls=0" src="https://www.youtube.com/embed/rNSIwjmynYQ?controls=0"
@@ -18,7 +18,7 @@ function AboutUs({ t }) {
allowFullScreen allowFullScreen
/> />
</div> </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> <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_1')}</p>
<p className="mb-16 text-lg text-common-layout font-light">{t('about_us_text_2')}</p> <p className="mb-16 text-lg text-common-layout font-light">{t('about_us_text_2')}</p>

View File

@@ -4,10 +4,10 @@ import { Link } from 'react-router-dom';
function ArticleCard({ t, id, title, description, image, updated }) { function ArticleCard({ t, id, title, description, image, updated }) {
return ( 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> <div>
<img <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} src={image}
alt={title} alt={title}
width="460" width="460"

View File

@@ -58,7 +58,7 @@ function FeedbackForm({ t }) {
<form <form
name="signinForm" name="signinForm"
noValidate 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)} onSubmit={handleSubmit(onSubmit)}
> >
<legend className="col-span-2 justify-self-center max-w-[860px] mb-8 text-4xl font-medium text-center"> <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 }) { function StatisticsCard({ title, text }) {
return ( 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> <h3 className="mb-52 text-[80px] font-semibold">{title}</h3>
<p className="text-xl leading-5 font-light">{text}</p> <p className="text-xl leading-5 font-light">{text}</p>
</article> </article>

View File

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

View File

@@ -1,7 +1,15 @@
import { styled } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import FusePageSimple from '@fuse/core/FusePageSimple'; import FusePageSimple from '@fuse/core/FusePageSimple';
import DemoContent from '@fuse/core/DemoContent'; import { Paper } from '@mui/material';
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 }) => ({ const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-header': { '& .FusePageSimple-header': {
@@ -16,21 +24,73 @@ const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-sidebarContent': {}, '& .FusePageSimple-sidebarContent': {},
})); }));
function FavoritesPage(props) { function FavoritesPage() {
const { t } = useTranslation('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 ( return (
<Root <Root
// header={
// <div className="p-24">
// <h4>{t('TITLE')}</h4>
// </div>
// }
content={ content={
<div className="p-24"> <div className="w-full p-60">
<h4>Content</h4> <Paper className="w-full h-320 mb-[30px] rounded-20 shadow-light" />
<br /> <PropertiesHeader
<DemoContent /> 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> </div>
} }
scroll="content" scroll="content"

View File

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

View File

@@ -1,7 +1,15 @@
import { styled } from '@mui/material/styles';
import { useTranslation } from 'react-i18next';
import FusePageSimple from '@fuse/core/FusePageSimple'; import FusePageSimple from '@fuse/core/FusePageSimple';
import DemoContent from '@fuse/core/DemoContent'; import { Paper } from '@mui/material';
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 }) => ({ const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-header': { '& .FusePageSimple-header': {
@@ -16,21 +24,83 @@ const Root = styled(FusePageSimple)(({ theme }) => ({
'& .FusePageSimple-sidebarContent': {}, '& .FusePageSimple-sidebarContent': {},
})); }));
function HistoryPage(props) { function HistoryPage() {
const { t } = useTranslation('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 ( return (
<Root <Root
// header={
// <div className="p-24">
// <h4>{t('TITLE')}</h4>
// </div>
// }
content={ content={
<div className="p-24"> <div className="w-full p-60">
<h4>Content</h4> <Paper className="w-full h-320 mb-[30px] rounded-20 shadow-light" />
<br /> <PropertiesHeader
<DemoContent /> 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> </div>
} }
scroll="content" scroll="content"

View File

@@ -1,11 +1,13 @@
import { lazy } from 'react';
import i18next from 'i18next'; import i18next from 'i18next';
import authRoles from '../../../configs/authRoles'; import authRoles from '../../../configs/consts';
import History from './History';
import en from './i18n/en'; import en from './i18n/en';
i18next.addResourceBundle('en', 'historyPage', en); i18next.addResourceBundle('en', 'historyPage', en);
const History = lazy(() => import('./History'));
const HistoryConfig = { const HistoryConfig = {
settings: { settings: {
layout: { layout: {
@@ -22,28 +24,3 @@ const HistoryConfig = {
}; };
export default 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,6 +104,7 @@ function ProfilePage({ t }) {
}); });
const { dirtyFields, errors } = formState; const { dirtyFields, errors } = formState;
// eslint-disable-next-line consistent-return
const uploadPicture = async (event) => { const uploadPicture = async (event) => {
const { target } = event; const { target } = event;
if (target.files && target.files[0]) { if (target.files && target.files[0]) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ export default class AuthService extends FuseUtils.EventEmitter {
.then((userCredential) => firebaseAuth.updateProfile(userCredential.user, { displayName })) .then((userCredential) => firebaseAuth.updateProfile(userCredential.user, { displayName }))
.then(() => { .then(() => {
const userRef = firebaseDb.ref(this.#db, `users/${this.#auth.currentUser.uid}`); 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); return firebaseDb.set(userRef, value);
}) })

View File

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

View File

@@ -2,7 +2,7 @@ const config = {
title: 'Layout 1 - Dashboard', title: 'Layout 1 - Dashboard',
defaults: { defaults: {
mode: 'container', mode: 'container',
containerWidth: 1570, containerWidth: 1590,
navbar: { navbar: {
display: true, display: true,
folded: 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 { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import NavbarLayout1Content from './NavbarLayout1Content'; import NavbarLayout1Content from './NavbarLayout1Content';
const navbarWidth = 280; const navbarWidth = 330;
const StyledNavBar = styled('div')(({ theme, open, position }) => ({ const StyledNavBar = styled('div')(({ theme, open, position }) => ({
minWidth: navbarWidth, minWidth: navbarWidth,

View File

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

View File

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

View File

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

View File

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