Newer
Older
import { makeStyles } from 'tss-react/mui';
import Container from '@mui/material/Container';
import FilterListIcon from '@mui/icons-material/FilterList';
import ClearIcon from '@mui/icons-material/Clear';
import ToggleButton from '@mui/material/ToggleButton';
import RadioButtonUncheckedRoundedIcon from '@mui/icons-material/RadioButtonUncheckedRounded';
import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded';
import CloseIcon from '@mui/icons-material/Close';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import Grid from '@mui/material/Grid';
import { withTracker } from 'meteor/react-meteor-data';
import Typography from '@mui/material/Typography';
import Chip from '@mui/material/Chip';
import Fade from '@mui/material/Fade';
import IconButton from '@mui/material/IconButton';
import Button from '@mui/material/Button';
import Slide from '@mui/material/Slide';
import AppBar from '@mui/material/AppBar';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Toolbar from '@mui/material/Toolbar';
import Dialog from '@mui/material/Dialog';
import List from '@mui/material/List';
import Divider from '@mui/material/Divider';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import Tooltip from '@mui/material/Tooltip';
import ServiceDetails from '../../components/services/ServiceDetails';
import Services from '../../../api/services/services';
import Categories from '../../../api/categories/categories';
import Spinner from '../../components/system/Spinner';
import { useAppContext } from '../../contexts/context';
import ServiceDetailsList from '../../components/services/ServiceDetailsList';
import { useIconStyles, DetaiIconCustom, SimpleIconCustom } from '../../components/system/icons/icons';
import { useStructure } from '../../../api/structures/hooks';
const useStyles = makeStyles()((theme, isMobile) => ({
flex: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
cardGrid: {
marginBottom: '0px',
},
chip: {
margin: theme.spacing(1),
'&&:hover,&&:focus': {
backgroundColor: theme.palette.backgroundFocus.main,
color: theme.palette.primary.main,
},
},
smallGrid: {
height: 20,
},
filterTitle: {
fontSize: '0.85rem',
margin: theme.spacing(1),
},
badge: {
height: 20,
display: 'flex',
padding: '0 6px',
flexWrap: 'wrap',
backgroundColor: theme.palette.primary.main,
color: `${theme.palette.tertiary.main} !important`,
minWidth: 20,
borderRadius: 10,
marginLeft: isMobile ? 10 : 0,
alignItems: 'center',
justifyContent: 'center',
},
invertedBadge: {
height: 20,
display: 'flex',
padding: '0 6px',
flexWrap: 'wrap',
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
backgroundColor: theme.palette.tertiary.main,
color: `${theme.palette.primary.main} !important`,
minWidth: 20,
borderRadius: 10,
marginLeft: isMobile ? 10 : 0,
alignItems: 'center',
justifyContent: 'center',
},
gridItem: {
display: 'flex',
justifyContent: 'center',
},
small: {
padding: '5px !important',
transition: 'all 300ms ease-in-out',
},
spaceBetween: {
display: 'flex',
justifyContent: 'space-between',
},
mobileButtonContainer: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: '0 !important',
},
categoryFilterMobile: {
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
left: 0,
backgroundColor: theme.palette.tertiary.main,
zIndex: theme.zIndex.modal,
},
categoriesList: {
marginTop: 60,
},
appBarBottom: {
bottom: 0,
top: 'auto',
backgroundColor: theme.palette.tertiary.main,
},
toolbarBottom: {
justifyContent: 'space-between',
},
emptyMsg: {
marginTop: 30,
marginBottom: 15,
},
}));
const Transition = React.forwardRef((props, ref) => <Slide direction="up" ref={ref} {...props} />);
export function ServicesPage({ services, categories, ready, structureMode, offline }) {
const [{ user, loadingUser, isMobile, servicePage }, dispatch] = useAppContext();
const structure = offline ? null : useStructure();
const { classes } = useStyles(isMobile);
const { classes: classesIcons } = useIconStyles();
const {
catList = [],
search = '',
viewMode = 'card', // Possible values : "card" or "list"
const favs = loadingUser || offline ? [] : user.favServices;
const updateGlobalState = (key, value) =>
dispatch({
type: 'servicePage',
data: {
...servicePage,
[key]: value,
},
});
const toggleFilter = () => updateGlobalState('filterToggle', !filterToggle);
const resetCatList = () => updateGlobalState('catList', []);
const changeViewMode = (_, value) => updateGlobalState('viewMode', value);
const updateCatList = (catId) => {
// Call by click on categories of services
if (catList.includes(catId)) {
// catId already in list so remove it
updateGlobalState(
'catList',
catList.filter((id) => id !== catId),
);
updateGlobalState('catList', [...catList, catId]);
const filterServices = (service) => {
let filterSearch = true;
let filterCat = true;
if (search) {
let searchText = service.title + service.description;
searchText = searchText.toLowerCase();
filterSearch = searchText.indexOf(search.toLowerCase()) > -1;
}
if (catList.length > 0) {
const intersection = catList.filter((value) => service.categories.includes(value));
filterCat = intersection.length > 0;
}
return filterSearch && filterCat;
};
const mapList = (func) => services.filter((service) => filterServices(service)).map(func);
const favAction = (id) => (favs.indexOf(id) === -1 ? 'fav' : 'unfav');
const toggleButtons = (
<ToggleButtonGroup value={viewMode} exclusive aria-label={i18n.__('pages.ServicesPage.viewMode')}>
<ToggleButton value="card" onClick={changeViewMode} aria-label={i18n.__('pages.ServicesPage.viewDetail')}>
<Tooltip title={i18n.__('pages.ServicesPage.viewDetail')} aria-label={i18n.__('pages.ServicesPage.viewDetail')}>
<span className={classesIcons.size}>
<DetaiIconCustom />
</span>
</Tooltip>
</ToggleButton>
<ToggleButton value="list" onClick={changeViewMode} aria-label={i18n.__('pages.ServicesPage.viewSimple')}>
<Tooltip title={i18n.__('pages.ServicesPage.viewSimple')} aria-label={i18n.__('pages.ServicesPage.viewSimple')}>
<span className={classesIcons.size}>
<SimpleIconCustom />
</span>
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
);
const mobileFilterButton = (
<Button
style={{ textTransform: 'none' }}
color="primary"
variant="outlined"
size="large"
onClick={toggleFilter}
startIcon={<FilterListIcon />}
>
{i18n.__('pages.ServicesPage.filter')}{' '}
{catList.length ? (
<span className={classes.badge}>{catList.length}</span>
) : (
i18n.__('pages.ServicesPage.emptyFilter')
)}
<Grid item xs={12} className={isMobile ? null : classes.flex}>
<Typography variant={isMobile ? 'h6' : 'h4'} className={classes.flex}>
{i18n.__(structureMode ? structure && structure.name : 'pages.ServicesPage.titleServices')}
<div className={classes.spaceBetween}>{!isMobile && toggleButtons}</div>
</Grid>
<Grid container spacing={4}>
<Grid item xs={12} sm={12} className={classes.mobileButtonContainer}>
{mobileFilterButton}
{toggleButtons}
</Grid>
<Typography variant="h6" display="inline" className={classes.filterTitle}>
{i18n.__('pages.ServicesPage.categories')}
</Typography>
{categories.map((cat) => (
<Chip
className={classes.chip}
key={cat._id}
label={cat.name}
<span className={catList.includes(cat._id) ? classes.invertedBadge : classes.badge}>
{cat.count}
</span>
onDelete={() => updateCatList(cat._id)}
variant={catList.includes(cat._id) ? 'default' : 'outlined'}
color={catList.includes(cat._id) ? 'primary' : 'default'}
onClick={() => updateCatList(cat._id)}
/>
))}
{catList.length > 0 ? (
<Button color="primary" onClick={resetCatList} startIcon={<ClearIcon />}>
{i18n.__('pages.ServicesPage.reset')}
</Button>
) : null}
<Grid container className={classes.cardGrid} spacing={isMobile ? 2 : 4}>
{services.length === 0 ? (
<Typography className={classes.emptyMsg}>
{i18n.__(`pages.ServicesPage.${structureMode ? 'NoStructureServices' : 'NoServices'}`)}
</Typography>
) : viewMode === 'list' && isMobile ? (
mapList((service) => (
<Grid className={classes.gridItem} item xs={4} md={2} key={service._id}>
<ServiceDetailsList service={service} favAction={favAction(service._id)} />
</Grid>
))
) : (
mapList((service) => (
<Grid className={classes.gridItem} item key={service._id} xs={12} sm={12} md={6} lg={4}>
<ServiceDetails
service={service}
favAction={favAction(service._id)}
updateCategories={updateCatList}
catList={catList}
categories={categories}
isShort={!isMobile && viewMode === 'list'}
/>
</Grid>
))
)}
<Dialog fullScreen open={filterToggle && isMobile} TransitionComponent={Transition}>
<AppBar className={classes.appBar}>
<Toolbar>
<IconButton edge="start" color="inherit" onClick={toggleFilter} aria-label="close" size="large">
<CloseIcon />
</IconButton>
<Typography variant="h6">{i18n.__('pages.ServicesPage.categories')}</Typography>
{!!catList.length && <span className={classes.invertedBadge}>{catList.length}</span>}
</Toolbar>
</AppBar>
<List className={classes.categoriesList}>
{categories.map((cat) => [
<ListItem button key={cat._id}>
<ListItemText
primary={cat.name}
onClick={() => updateCatList(cat._id)}
secondary={`${cat.count} applications`}
/>
<ListItemSecondaryAction>
<IconButton edge="end" aria-label="add" onClick={() => updateCatList(cat._id)} size="large">
{catList.includes(cat._id) ? (
<CheckCircleRoundedIcon fontSize="large" color="primary" />
) : (
<RadioButtonUncheckedRoundedIcon fontSize="large" color="primary" />
)}
</IconButton>
</ListItemSecondaryAction>
</ListItem>,
<Divider key={`${cat._id}divider`} />,
])}
</List>
<AppBar className={classes.appBarBottom}>
<Toolbar className={classes.toolbarBottom}>
<Button
color="primary"
disabled={catList.length === 0}
variant="outlined"
onClick={resetCatList}
startIcon={<ClearIcon />}
>
{i18n.__('pages.ServicesPage.reset')}
</Button>
<Button
color="primary"
variant="contained"
onClick={toggleFilter}
startIcon={<CheckCircleRoundedIcon />}
>
{i18n.__('pages.ServicesPage.validate')}
</Button>
</Toolbar>
</AppBar>
</Dialog>
ServicesPage.propTypes = {
services: PropTypes.arrayOf(PropTypes.object).isRequired,
structureMode: PropTypes.bool.isRequired,
offline: PropTypes.bool,
};
ServicesPage.defaultProps = {
offline: false,
const structureMode = path === '/structure';
const [{ structure }] = useAppContext();
/**
* - Grab current user structure with the ancestors
*
* - This is used to get all services from top level to current one
*/
const currentStructureWithAncestors = [];
if (structure && structure._id) {
currentStructureWithAncestors.push(structure._id, ...structure.ancestorsIds);
}
const subName = structureMode ? 'services.structure.ids' : 'services.all';
const servicesHandle = Meteor.subscribe(subName, structureMode && { structureIds: currentStructureWithAncestors });
services = Services.findFromPublication(subName, { state: { $ne: 10 } }, { sort: { title: 1 } }).fetch();
} else {
services = Services.find({ state: { $ne: 10 } }, { sort: { title: 1 } }).fetch();
}
const cats = Categories.find({}, { sort: { name: 1 } }).fetch();
const categories = cats.map((cat) => ({
...cat,
count: Services.find({ state: { $ne: 10 }, categories: { $in: [cat._id] } }).count(),
}));
const ready = servicesHandle.ready() && categoriesHandle.ready();
structureMode,