all repos — caroster @ e5c55de093d4edc0ba99de71ea4ce6b32179268a

[Octree] Group carpool to your event https://caroster.io

🐛 Fixes after demo

#254
Simon Mulquin simon@octree.ch
Mon, 14 Feb 2022 16:37:03 +0000
commit

e5c55de093d4edc0ba99de71ea4ce6b32179268a

parent

63428b58d9a5b0438d9e4d8afb2fd46e9dab1a08

M frontend/containers/GenericMenu/index.tsxfrontend/containers/GenericMenu/index.tsx

@@ -3,7 +3,7 @@ import {useTranslation} from 'react-i18next';

import useAuthStore from '../../stores/useAuthStore'; import useProfile from '../../hooks/useProfile'; import useSettings from '../../hooks/useSettings'; -import Languages from '../Languages'; +import Languages from '../Languages/MenuItem'; import Action from './Action'; const GenericMenu = ({anchorEl, setAnchorEl, actions = []}) => {
A frontend/containers/Languages/Icon.tsx

@@ -0,0 +1,63 @@

+import {useState} from 'react'; +import Box from '@material-ui/core/Box'; +import IconButton from '@material-ui/core/IconButton'; +import Icon from '@material-ui/core/Icon'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import {useTranslation} from 'react-i18next'; +import {Enum_Userspermissionsuser_Lang} from '../../generated/graphql'; +import withLanguagesSelection, { + LanguageSelectionComponentProps, +} from './withLanguagesSelection'; + +const IconLanguageSelection = ({ + language, + setLanguage, + onConfirmCallback, +}: LanguageSelectionComponentProps) => { + const {t} = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + + const handleClick = event => { + setAnchorEl(event.currentTarget); + }; + + const onConfirm = (lang: Enum_Userspermissionsuser_Lang) => { + setLanguage(lang); + setAnchorEl(null); + + onConfirmCallback(lang); + }; + + return ( + <> + <Box position="fixed" top={0} right={0} zIndex={1050} p={1}> + <IconButton + color="primary" + aria-label="Languages" + onClick={handleClick} + > + <Icon>language</Icon> + </IconButton> + </Box> + <Menu + id="LanguagesMenu" + anchorEl={anchorEl} + keepMounted + open={Boolean(anchorEl)} + onClose={() => setAnchorEl(null)} + > + <MenuItem + disabled={language === Enum_Userspermissionsuser_Lang.Fr} + onClick={() => onConfirm(Enum_Userspermissionsuser_Lang.Fr)} + >{t`languages.fr`}</MenuItem> + <MenuItem + disabled={language === Enum_Userspermissionsuser_Lang.En} + onClick={() => onConfirm(Enum_Userspermissionsuser_Lang.En)} + >{t`languages.en`}</MenuItem> + </Menu> + </> + ); +}; + +export default withLanguagesSelection(IconLanguageSelection);
A frontend/containers/Languages/MenuItem.tsx

@@ -0,0 +1,59 @@

+import {useState} from 'react'; +import MenuList from '@material-ui/core/MenuList'; +import MenuItem from '@material-ui/core/MenuItem'; +import {makeStyles} from '@material-ui/core/styles'; +import {useTranslation} from 'react-i18next'; +import { + Enum_Userspermissionsuser_Lang, +} from '../../generated/graphql'; +import withLanguagesSelection, { + LanguageSelectionComponentProps, +} from './withLanguagesSelection'; + +const Languages = ({ + language, + setLanguage, + onConfirmCallback, +}: LanguageSelectionComponentProps) => { + const {t} = useTranslation(); + const [isSelecting, setSelecting] = useState(false); + const {languagesList} = useStyles({isSelecting}); + + const handleClick = event => { + setSelecting(!isSelecting); + }; + + const onConfirm = (lang: Enum_Userspermissionsuser_Lang) => { + setLanguage(lang); + setSelecting(false); + + onConfirmCallback(lang); + }; + + return ( + <> + <MenuItem onClick={handleClick}>{t('menu.language')}</MenuItem> + <MenuList className={languagesList} dense> + <MenuItem + disabled={language === Enum_Userspermissionsuser_Lang.Fr} + onClick={() => onConfirm(Enum_Userspermissionsuser_Lang.Fr)} + >{t`languages.fr`}</MenuItem> + <MenuItem + disabled={language === Enum_Userspermissionsuser_Lang.En} + onClick={() => onConfirm(Enum_Userspermissionsuser_Lang.En)} + >{t`languages.en`}</MenuItem> + </MenuList> + </> + ); +}; + +const useStyles = makeStyles(theme => ({ + languagesList: ({isSelecting}: {isSelecting: boolean}) => ({ + visibility: isSelecting ? 'visible' : 'hidden', + maxHeight: isSelecting ? 'none' : 0, + padding: isSelecting ? `0 ${theme.spacing(0.5)}px` : 0, + overflow: 'hidden', + }), +})); + +export default withLanguagesSelection(Languages);
D frontend/containers/Languages/index.tsx

@@ -1,86 +0,0 @@

-import {useState, useEffect} from 'react'; -import moment from 'moment'; -import MenuList from '@material-ui/core/MenuList'; -import MenuItem from '@material-ui/core/MenuItem'; -import {makeStyles} from '@material-ui/core/styles'; -import {useTranslation} from 'react-i18next'; -import useLangStore from '../../stores/useLangStore'; -import useProfile from '../../hooks/useProfile'; -import { - useUpdateMeMutation, - Enum_Userspermissionsuser_Lang, -} from '../../generated/graphql'; - -const Languages = () => { - const {t, i18n} = useTranslation(); - const [isSelecting, setSelecting] = useState(false); - const language = useLangStore(s => s.language); - const setLanguage = useLangStore(s => s.setLanguage); - const {profile, connected} = useProfile(); - const [updateProfile] = useUpdateMeMutation(); - const { languagesList } = useStyles({ isSelecting }); - - useEffect(() => { - if (navigator?.language === 'en') - setLanguage(Enum_Userspermissionsuser_Lang.En); - }, []); - - useEffect(() => { - const momentLang = language === 'FR' ? 'fr-ch' : 'en'; - moment.locale(momentLang); - i18n.changeLanguage(language?.toLowerCase()); - }, [language]); - - useEffect(() => { - if (profile?.lang) setLanguage(profile.lang); - }, [profile]); - - const handleClick = event => { - setSelecting(!isSelecting); - }; - - const onConfirm = (lang: Enum_Userspermissionsuser_Lang) => { - setLanguage(lang); - setSelecting(false); - - if (connected) { - updateProfile({ - variables: { - userUpdate: { - lang, - }, - }, - }); - } - }; - - return ( - <> - <MenuItem onClick={handleClick} > - {t('menu.language')} - </MenuItem> - <MenuList className={languagesList} dense> - <MenuItem - disabled={language === Enum_Userspermissionsuser_Lang.Fr} - onClick={() => onConfirm(Enum_Userspermissionsuser_Lang.Fr)} - >{t`languages.fr`}</MenuItem> - <MenuItem - disabled={language === Enum_Userspermissionsuser_Lang.En} - onClick={() => onConfirm(Enum_Userspermissionsuser_Lang.En)} - >{t`languages.en`}</MenuItem> - </MenuList> - </> - ); -}; - - -const useStyles = makeStyles(theme => ({ - languagesList: ({ isSelecting }: { isSelecting: boolean }) => ({ - visibility: isSelecting ? 'visible' : 'hidden', - maxHeight: isSelecting ? 'none' : 0, - padding: isSelecting ? `0 ${theme.spacing(.5)}px` : 0, - overflow: 'hidden', - }), -})); - -export default Languages;
A frontend/containers/Languages/withLanguagesSelection.tsx

@@ -0,0 +1,68 @@

+import {useEffect} from 'react'; +import useLangStore from '../../stores/useLangStore'; +import useProfile from '../../hooks/useProfile'; +import { + useUpdateMeMutation, + Enum_Userspermissionsuser_Lang, +} from '../../generated/graphql'; +import moment from 'moment'; +import {useTranslation} from 'react-i18next'; + +type LangFunction = (lang: Enum_Userspermissionsuser_Lang) => void; + +export interface LanguageSelectionComponentProps { + language: Enum_Userspermissionsuser_Lang; + setLanguage: LangFunction; + onConfirmCallback: LangFunction; +} + +const withLanguagesSelection = + ( + LanguageSelectionComponent: ( + args: LanguageSelectionComponentProps + ) => JSX.Element + ) => + () => { + const {i18n} = useTranslation(); + const language = useLangStore(s => s.language); + const setLanguage = useLangStore(s => s.setLanguage); + const {profile, connected} = useProfile(); + const [updateProfile] = useUpdateMeMutation(); + + useEffect(() => { + if (i18n.language === 'en') + setLanguage(Enum_Userspermissionsuser_Lang.En); + }, []); + + useEffect(() => { + const momentLang = language === 'FR' ? 'fr-ch' : 'en'; + moment.locale(momentLang); + i18n.changeLanguage(language?.toLowerCase()); + }, [language]); + + useEffect(() => { + if (profile?.lang) setLanguage(profile.lang); + }, [profile]); + + const onConfirmCallback = (lang: Enum_Userspermissionsuser_Lang) => { + if (connected) { + updateProfile({ + variables: { + userUpdate: { + lang, + }, + }, + }); + } + }; + + return ( + <LanguageSelectionComponent + language={language} + setLanguage={setLanguage} + onConfirmCallback={onConfirmCallback} + /> + ); + }; + +export default withLanguagesSelection;
M frontend/containers/PassengersList/Passenger.tsxfrontend/containers/PassengersList/Passenger.tsx

@@ -21,14 +21,25 @@ const {t} = useTranslation();

const classes = useStyles(); const {user} = useProfile(); - const isUser = user && user.id === passenger?.user?.id; - const showLocation = isVehicle ? false : passenger.location + const isUser = user && `${user.id}` === passenger?.user?.id; + const showLocation = isVehicle ? false : passenger.location; if (passenger) { return ( <> <ListItemText - primary={<>{passenger.name}{isUser && <Chip className={classes.me} label={t('generic.me')} variant="outlined" />}</>} + primary={ + <> + {passenger.name} + {isUser && ( + <Chip + className={classes.me} + label={t('generic.me')} + variant="outlined" + /> + )} + </> + } secondary={showLocation} /> {button}

@@ -58,7 +69,7 @@ color: theme.palette.text.secondary,

}, me: { marginLeft: theme.spacing(2), - } + }, })); export default Passenger;
M frontend/containers/PassengersList/index.tsxfrontend/containers/PassengersList/index.tsx

@@ -18,6 +18,7 @@ disabled?: boolean;

}) => JSX.Element; disabled?: boolean; isVehicle?: boolean; + isTravel?: boolean; places?: number; onPress?: (passengerId: string) => void; onClick?: (passengerId: string) => void;
M frontend/containers/Travel/Header.tsxfrontend/containers/Travel/Header.tsx

@@ -6,6 +6,7 @@ import moment from 'moment';

import {useTranslation} from 'react-i18next'; import Link from '@material-ui/core/Link'; import {Travel} from '../../generated/graphql'; +import getMapsLink from '../../utils/getMapsLink'; interface Props { travel: Travel;

@@ -56,7 +57,7 @@ <Link

component="a" target="_blank" rel="noopener noreferrer" - href={`https://maps.google.com/?q=${encodeURI(travel.meeting)}`} + href={getMapsLink(travel.meeting)} > {travel.meeting} </Link>
M frontend/containers/Travel/index.tsxfrontend/containers/Travel/index.tsx

@@ -2,13 +2,13 @@ import {useReducer} from 'react';

import {makeStyles} from '@material-ui/core/styles'; import Divider from '@material-ui/core/Divider'; import Paper from '@material-ui/core/Paper'; +import {Travel as TravelType} from '../../generated/graphql'; +import ClearButton from '../ClearButton'; import PassengersList from '../PassengersList'; import AddPassengerButtons from '../AddPassengerButtons'; import HeaderEditing from './HeaderEditing'; import Header from './Header'; import useActions from './useActions'; -import {Travel as TravelType} from '../../generated/graphql'; -import ClearButton from '../ClearButton'; interface Props { travel: TravelType;
M frontend/containers/VehicleChoiceDialog/index.tsxfrontend/containers/VehicleChoiceDialog/index.tsx

@@ -62,7 +62,7 @@ </Fragment>

))} </List> )) || ( - <Container> + <Container className={classes.empty}> <Typography>{t('travel.vehicle.empty')}</Typography> </Container> )}

@@ -99,6 +99,9 @@ },

new: { maxWidth: '300px', }, + empty: { + padding: theme.spacing(2, 3) + } })); export default VehicleChoiceDialog;
M frontend/containers/WaitingList/AssignButton.tsxfrontend/containers/WaitingList/AssignButton.tsx

@@ -14,7 +14,6 @@ const AssignButton = (props: Props) => {

const {onClick, tabIndex} = props; const classes = useStyles(); const {t} = useTranslation(); - console.log(props.disabled) return ( <ListItemSecondaryAction className={classes.action} onClick={onClick} tabIndex={tabIndex}>
M frontend/containers/WaitingList/TravelDialog.tsxfrontend/containers/WaitingList/TravelDialog.tsx

@@ -1,4 +1,4 @@

-import {forwardRef} from 'react'; +import {forwardRef, useEffect} from 'react'; import moment from 'moment'; import Link from '@material-ui/core/Link'; import Typography from '@material-ui/core/Typography';

@@ -12,13 +12,29 @@ import List from '@material-ui/core/List';

import IconButton from '@material-ui/core/IconButton'; import Icon from '@material-ui/core/Icon'; import Box from '@material-ui/core/Box'; +import Container from '@material-ui/core/Container'; import {makeStyles} from '@material-ui/core/styles'; import {useTranslation} from 'react-i18next'; +import {ComponentPassengerPassenger, Travel} from '../../generated/graphql'; +import getMapsLink from '../../utils/getMapsLink'; -const TravelDialog = ({travels, open, onClose, onSelect}) => { +interface Props { + travels: Array<Travel>; + passenger: ComponentPassengerPassenger; + open: boolean; + onClose: () => void; + onSelect: (travel: Travel) => void; +} + +const TravelDialog = ({travels, passenger, open, onClose, onSelect}: Props) => { const classes = useStyles(); const {t} = useTranslation(); + const availableTravels = travels?.filter( + travel => + travel.passengers && travel?.vehicle?.seats > travel.passengers.length + ); + return ( <Dialog fullScreen

@@ -36,56 +52,58 @@ {t('passenger.creation.available_cars')}

</Typography> </Toolbar> </AppBar> - <div className={classes.offset}> - <List disablePadding> - {travels?.map((travel, i) => { - const passengers = travel.passengers ? travel.passengers.length : 0; - const counter = `${passengers} / ${travel?.vehicle?.seats}`; - return ( - <ListItem - key={i} - divider - disabled={passengers === travel.seats} - className={classes.listItem} - > - <Box className={classes.rtlBox}> - <Box className={classes.info}> - <Typography variant="subtitle1" className={classes.date}> - {t('passenger.creation.departure')} - {moment(travel.departure).format('LLLL')} - </Typography> - <Link - target="_blank" - rel="noreferrer" - href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent( - travel.meeting - )}`} - onClick={e => e.preventDefault} - > - {travel.meeting} - </Link> - </Box> - <Box className={classes.info}> - <Typography variant="h6">{travel.vehicle?.name}</Typography> - <Typography variant="body2"> - {t('passenger.creation.seats', {seats: counter})} - </Typography> + {(availableTravels.length === 0 && ( + <Typography className={classes.noTravel}> + {t('passenger.creation.no_travel', {name: passenger?.name})} + </Typography> + )) || ( + <div className={classes.offset}> + <List disablePadding> + {availableTravels.map((travel, i) => { + const passengersCount = travel?.passengers?.length || 0; + const counter = `${passengersCount} / ${ + travel?.vehicle?.seats || 0 + }`; + return ( + <ListItem key={i} divider className={classes.listItem}> + <Box className={classes.rtlBox}> + <Box className={classes.info}> + <Typography variant="subtitle1" className={classes.date}> + {t('passenger.creation.departure')} + {moment(travel.departure).format('LLLL')} + </Typography> + <Link + target="_blank" + rel="noreferrer" + href={getMapsLink(travel.meeting)} + onClick={e => e.preventDefault} + > + {travel.meeting} + </Link> + </Box> + <Box className={classes.info}> + <Typography variant="h6"> + {travel.vehicle?.name} + </Typography> + <Typography variant="body2"> + {t('passenger.creation.seats', {seats: counter})} + </Typography> + </Box> </Box> - </Box> - <Button - color="primary" - variant="contained" - disabled={travel?.vehicle?.seats === passengers} - onClick={() => onSelect(travel)} - className={classes.button} - > - {t('passenger.creation.assign')} - </Button> - </ListItem> - ); - })} - </List> - </div> + <Button + color="primary" + variant="contained" + onClick={() => onSelect(travel)} + className={classes.button} + > + {t('passenger.creation.assign')} + </Button> + </ListItem> + ); + })} + </List> + </div> + )} </Dialog> ); };

@@ -132,6 +150,10 @@ },

button: { padding: theme.spacing(1, 15), margin: theme.spacing(1), + }, + noTravel: { + margin: '45vh auto', + textAlign: 'center', }, }));
M frontend/containers/WaitingList/index.tsxfrontend/containers/WaitingList/index.tsx

@@ -152,7 +152,6 @@ <PassengersList

passengers={event.waitingList} onPress={onPress} Button={ListButton} - disabled={!isEditing && availability <= 0} /> </Paper> <RemoveDialog

@@ -171,6 +170,7 @@ onRemove={onRemove}

/> <TravelDialog travels={travels} + passenger={addingPassenger} open={!!addingPassenger} onClose={() => setAddingPassenger(null)} onSelect={selectTravel}
M frontend/i18n.tsfrontend/i18n.ts

@@ -12,11 +12,14 @@ translation: translationEn,

}, }; +const getUserLng = () => typeof window !== 'undefined' && typeof window.navigator !== 'undefined' ? navigator.language : 'en'; + i18n .use(initReactI18next) // passes i18n down to react-i18next .init({ resources, - lng: 'fr', + lng: getUserLng(), + supportedLngs: ['fr', 'en'], interpolation: { escapeValue: false, // react already safes from xss },
M frontend/locales/en.jsonfrontend/locales/en.json

@@ -230,7 +230,8 @@ "creation": {

"seats": "Number of passengers: {{seats}}", "departure": "Departure: ", "assign": "Assign", - "available_cars": "Available cars" + "available_cars": "Available cars", + "no_travel": "No available cars. {{name}} will receive an email when new cars will be available." }, "actions": { "remove_alert": "Are you sure you want to remove <italic> <bold> {{name}} </bold> </italic> from the waitlist?",
M frontend/locales/fr.jsonfrontend/locales/fr.json

@@ -229,7 +229,8 @@ "creation": {

"seats": "Nombre de passagers: {{seats}}", "departure": "Depart: ", "assign": "Placer", - "available_cars": "Voitures disponibles" + "available_cars": "Voitures disponibles", + "no_travel": "Aucune place disponible. {{name}} recevra un email lorsque des places seront ajoutées." }, "actions": { "remove_alert": "Voulez-vous vraiment supprimer <italic><bold>{{name}}</bold></italic> de la liste d'attente ?",
M frontend/pages/_document.tsxfrontend/pages/_document.tsx

@@ -6,7 +6,7 @@

export default class MyDocument extends Document { render() { return ( - <Html lang="fr"> + <Html> <Head> <meta name="theme-color" content={theme.palette.primary.main} /> <meta name="application-name" content="Caroster" />
M frontend/pages/auth/login.tsxfrontend/pages/auth/login.tsx

@@ -9,6 +9,7 @@ import Layout from '../../layouts/Centered';

import Logo from '../../components/Logo'; import SignInForm from '../../containers/SignInForm'; import LoginGoogle from '../../containers/LoginGoogle'; +import LanguagesIcon from '../../containers/Languages/Icon'; const Login = () => { const {t} = useTranslation();

@@ -27,6 +28,7 @@ <SignInForm />

<Divider /> <LoginGoogle /> </Card> + <LanguagesIcon/> </Layout> ); };
A frontend/utils/getMapsLink.ts

@@ -0,0 +1,6 @@

+const getMapsLink = (address: string) => + `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent( + address + )}`; + +export default getMapsLink;