all repos — caroster @ 1410b54dc70ad59a92a3fe31c76237c46a37c1d8

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

🐛 Fixes after PO review #324 #468 #413 #421
Maylie maylie@octree.ch
Wed, 06 Mar 2024 09:41:39 +0000
commit

1410b54dc70ad59a92a3fe31c76237c46a37c1d8

parent

313dd5f41e70cb7c57988fe4e0dd361d444e9ed7

M backend/src/api/passenger/policies/check-creation.tsbackend/src/api/passenger/policies/check-creation.ts

@@ -3,8 +3,9 @@

export default async (policyContext, _config, { strapi }) => { const user = policyContext.state.user; const eventId = policyContext.args?.data?.event; + + if (!eventId) throw new errors.ValidationError(`No event ID provided`); const event = await strapi.entityService.findOne("api::event.event", eventId); - if (!event) throw new errors.NotFoundError(`Event not found`); if (event.enabled_modules?.includes("caroster-plus")) {
M frontend/containers/Alerts/AlertsForm.tsxfrontend/containers/Alerts/AlertsForm.tsx

@@ -58,7 +58,6 @@ <Stack display="flex" direction="column" spacing={2}>

<FormControl> <PlaceInput label={t('alert.location.label')} - textFieldProps={{sx: {mt: 2}}} place={address} latitude={latitude} longitude={longitude}
M frontend/containers/Alerts/index.tsxfrontend/containers/Alerts/index.tsx

@@ -1,10 +1,11 @@

import {useReducer} from 'react'; -import {Box, Container, Paper, useMediaQuery} from '@mui/material'; +import {Box, Container, Paper, Typography, useMediaQuery} from '@mui/material'; import theme from '../../theme'; import {EventEntity, TripAlertEntity} from '../../generated/graphql'; import AlertsHeader from './AlertsHead'; import AlertsForm from './AlertsForm'; +import {useTranslation} from 'react-i18next'; interface Props { event: EventEntity;

@@ -17,6 +18,7 @@ const [switchChecked, handleToggle] = useReducer(

i => !i, tripAlertEntity?.attributes.enabled || false ); + const {t} = useTranslation(); return ( <Container maxWidth="sm" sx={{mt: 11, mx: 0, px: isMobile ? 2 : 4}}>

@@ -28,6 +30,9 @@ switchChecked={switchChecked}

handleToggle={handleToggle} disabled={switchChecked} /> + <Typography sx={{mt: 2}} variant="body2"> + {t('alert.description')} + </Typography> <AlertsForm event={event} disabled={!switchChecked}
M frontend/containers/DrawerNotification/CardNotification.tsxfrontend/containers/DrawerNotification/CardNotification.tsx

@@ -44,9 +44,8 @@ paddingBottom={1}

direction="row" display="flex" justifyContent="space-between" - spacing={2} > - <Box display="flex" alignItems="center" sx={{width: '168px'}}> + <Box display="flex" alignItems="center" sx={{width: '192px'}}> {!notification.attributes.read && ( <Badge sx={{pr: 2}}

@@ -58,12 +57,12 @@ horizontal: 'left',

}} /> )} - <Typography variant="subtitle1" noWrap> + <Typography variant="body2" color="text.secondary" noWrap> {eventName} </Typography> </Box> - <Typography variant="overline" color="text.secondary"> + <Typography variant="body2" color="text.secondary"> {formatDate(notification.attributes.createdAt)} </Typography> </Stack>
M frontend/containers/DrawerPassenger/index.tsxfrontend/containers/DrawerPassenger/index.tsx

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

-import { - Drawer, - Icon, - Typography, - useMediaQuery, - Link, - Box, -} from '@mui/material'; +import {Drawer, Typography, useMediaQuery, Link, Box} from '@mui/material'; import {useTranslation} from 'react-i18next'; import DrawerPassengerHeader from './DrawerPassengerHeader';

@@ -67,32 +60,35 @@ <Typography variant="body1" gutterBottom>

{lastName} </Typography> </Box> - {phone && ( - <Box display="flex" flexDirection="column"> - <Typography variant="h6"> - {t('passenger.informations.phone.label')} - </Typography> - <Link href={`tel:${phone}`}> - <Typography variant="body1" gutterBottom> - {phone} - </Typography> - </Link> - </Box> - )} + <Box display="flex" flexDirection="column"> <Typography variant="h6"> {t('passenger.informations.email.label')} </Typography> - <Typography variant="body1" gutterBottom> + <Link + sx={{display: 'flex', flexDirection: 'row', gap: 1}} + href={`mailto:${email}`} + > {email} + </Link> + </Box> + <Box display="flex" flexDirection="column"> + <Typography variant="h6"> + {t('passenger.informations.phone.label')} </Typography> + {phone ? ( + <Link + sx={{display: 'flex', flexDirection: 'row', gap: 1}} + href={`tel:${phone}`} + > + {phone} + </Link> + ) : ( + <Typography variant="body1"> + {t('passenger.informations.notSpecify')} + </Typography> + )} </Box> - <Link - sx={{display: 'flex', flexDirection: 'row', gap: 1}} - href={`mailto:${email}`} - > - <Icon>email</Icon> {t('passenger.informations.email.label')} - </Link> </Box> </Box> </Drawer>
M frontend/containers/PassengersList/Passenger.tsxfrontend/containers/PassengersList/Passenger.tsx

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

-import {ReactNode, useReducer} from 'react'; +import {ReactNode} from 'react'; import { ListItemAvatar, ListItemIcon,

@@ -12,57 +12,40 @@ } from '@mui/material';

import {useTranslation} from 'react-i18next'; import useProfile from '../../hooks/useProfile'; import {PassengerEntity} from '../../generated/graphql'; -import DrawerPassenger from '../DrawerPassenger'; -import usePermissions from '../../hooks/usePermissions'; interface Props { passenger?: PassengerEntity; - button?: ReactNode; isTravel?: boolean; + Actions?: (props: {passenger: PassengerEntity}) => ReactNode; } const Passenger = (props: Props) => { - const {passenger, button, isTravel} = props; + const {passenger, isTravel, Actions} = props; const theme = useTheme(); const {t} = useTranslation(); - - const [openDrawerPassenger, toggleDrawerPassenger] = useReducer( - i => !i, - false - ); - - const { - userPermissions: {canSeePassengerDetails}, - } = usePermissions(); const {userId} = useProfile(); const isUser = `${userId}` === passenger?.attributes.user?.data?.id; - const showLocation = isTravel ? null : ( - <Typography - sx={{pl: 1, color: 'GrayText'}} - component="span" - variant="caption" - > - {passenger.attributes.location} - </Typography> - ); if (passenger) { return ( - <Box sx={{width: 1}} aria-label="user informations"> + <Box + aria-label="user informations" + sx={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + width: '100%', + padding: 0, + }} + > <ListItemText primary={ <Box - onClick={toggleDrawerPassenger} sx={{ - width: 1, - maxWidth: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', - cursor: canSeePassengerDetails(passenger) - ? 'pointer' - : 'inherit', }} > <Icon fontSize="inherit" sx={{verticalAlign: 'middle', mr: 0.5}}>

@@ -73,7 +56,6 @@ component="span"

variant="body1" sx={{ overflow: 'hidden', - maxWidth: 'calc(100% - 88px)', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }}

@@ -87,21 +69,19 @@ label={t('generic.me')}

variant="outlined" /> )} - {showLocation} + {!isTravel && ( + <Typography + sx={{pl: 1, color: 'GrayText'}} + component="span" + variant="caption" + > + {passenger.attributes.location} + </Typography> + )} </Box> } /> - {button} - {canSeePassengerDetails(passenger) && ( - <DrawerPassenger - isOpen={openDrawerPassenger} - onClose={() => toggleDrawerPassenger()} - firstName={passenger?.attributes.user?.data?.attributes.firstName} - lastName={passenger?.attributes.user?.data?.attributes.lastName} - email={passenger?.attributes.email} - phone={passenger?.attributes.phone} - /> - )} + <Actions passenger={passenger} /> </Box> ); } else {
M frontend/containers/PassengersList/index.tsxfrontend/containers/PassengersList/index.tsx

@@ -1,7 +1,7 @@

-import {useState} from 'react'; import {ListItem, List, styled, useTheme} from '@mui/material'; import Passenger from './Passenger'; import {PassengerEntity, TravelEntity} from '../../generated/graphql'; +import {ReactNode} from 'react'; const PREFIX = 'PassengersList';

@@ -20,25 +20,15 @@ paddingRight: theme.spacing(12),

}, })); -export type PassengerButton = ({ - onClick, - disabled, -}: { - onClick: () => void; - passenger?: PassengerEntity; - disabled?: boolean; -}) => JSX.Element; - interface Props { passengers: PassengerEntity[]; - Button: PassengerButton; travel?: TravelEntity; - onPress?: (passengerId: string) => void; - onClick?: (passengerId: string) => void; + onClickPassenger?: (passengerId: string) => void; + Actions?: (props: {passenger: PassengerEntity}) => ReactNode; } const PassengersList = (props: Props) => { - const {passengers, Button, onClick, onPress, travel} = props; + const {passengers, onClickPassenger, travel, Actions} = props; const theme = useTheme(); return (

@@ -47,10 +37,9 @@ <List disablePadding>

{!!passengers && passengers.map((passenger, index) => ( <ListItem - sx={{paddingRight: theme.spacing(12)}} key={index} - button={!!onPress} - onClick={() => !!onPress && onPress(passenger.id)} + button={!!onClickPassenger} + onClick={() => onClickPassenger?.(passenger.id)} > <Passenger key={index}

@@ -59,12 +48,7 @@ id: passenger.id,

attributes: {...passenger.attributes, travel: {data: travel}}, }} isTravel={!!travel} - button={ - <Button - passenger={passenger} - onClick={() => onClick && onClick(passenger.id)} - /> - } + Actions={Actions} /> </ListItem> ))}
A frontend/containers/Travel/PassengerActions.tsx

@@ -0,0 +1,48 @@

+import {Box, Icon, IconButton} from '@mui/material'; +import usePermissions from '../../hooks/usePermissions'; +import useActions from './useActions'; +import {PassengerEntity, TravelEntity} from '../../generated/graphql'; + +type Props = { + passenger: PassengerEntity; + travel: TravelEntity; + setFocusPassenger: (passenger: PassengerEntity) => void; +}; + +const PassengerActions = (props: Props) => { + const {passenger, travel, setFocusPassenger} = props; + const { + userPermissions: {canDeletePassenger, canSeePassengerDetails}, + } = usePermissions(); + const actions = useActions({travel}); + + return ( + <Box display="flex"> + {canDeletePassenger(passenger) && { + id: passenger.id, + attributes: { + ...passenger.attributes, + travel: {data: travel}, + }, + } && ( + <IconButton + color="primary" + onClick={() => actions.removePassengerFromTravel(passenger.id)} + tabIndex={-1} + > + <Icon>delete_outline</Icon> + </IconButton> + )} + {canSeePassengerDetails(passenger) && ( + <IconButton + color="primary" + onClick={() => setFocusPassenger(passenger)} + > + <Icon>info_outlined</Icon> + </IconButton> + )} + </Box> + ); +}; + +export default PassengerActions;
M frontend/containers/Travel/index.tsxfrontend/containers/Travel/index.tsx

@@ -1,21 +1,20 @@

-import {useMemo, useReducer} from 'react'; +import {useMemo, useReducer, useState} from 'react'; import Divider from '@mui/material/Divider'; import Paper from '@mui/material/Paper'; -import Button from '@mui/material/Button'; -import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; import {useTheme} from '@mui/styles'; -import {useTranslation} from 'react-i18next'; import HeaderEditing from './HeaderEditing'; import Header from './Header'; import RequestTripModal from './RequestTripModal'; -import useActions from './useActions'; import PassengersList from '../PassengersList'; import AddPassengerButtons from '../AddPassengerButtons'; import useProfile from '../../hooks/useProfile'; import usePermissions from '../../hooks/usePermissions'; import useMapStore from '../../stores/useMapStore'; import useEventStore from '../../stores/useEventStore'; -import {TravelEntity} from '../../generated/graphql'; +import {PassengerEntity, TravelEntity} from '../../generated/graphql'; +import DrawerPassenger from '../DrawerPassenger'; +import PassengerActions from './PassengerActions'; + interface Props { travel: TravelEntity; onAddSelf: () => void;

@@ -28,21 +27,21 @@ const isCarosterPlus = useEventStore(s =>

s.event.enabled_modules?.includes('caroster-plus') ); const { - userPermissions: {canDeletePassenger, canJoinTravels, canAddToTravel}, + userPermissions: {canSeePassengerDetails, canJoinTravels, canAddToTravel}, } = usePermissions(); - const {t} = useTranslation(); const theme = useTheme(); const [isEditing, toggleEditing] = useReducer(i => !i, false); const [requestTripModalOpen, toggleRequestTripModal] = useReducer( i => !i, false ); - const actions = useActions({travel}); const {userId, connected} = useProfile(); const {focusedTravel} = useMapStore(); const focused = focusedTravel === travel.id; const disableNewPassengers = travel.attributes.passengers?.data?.length >= travel.attributes.seats; + + const [focusPassenger, setFocusPassenger] = useState<PassengerEntity>(); const registered = useMemo(() => { if (!connected) return false;

@@ -93,21 +92,29 @@ )}

{travel.attributes.passengers.data.length > 0 && <Divider />} <PassengersList passengers={travel.attributes.passengers.data} - onClick={actions.removePassengerFromTravel} travel={travel} - Button={({onClick, passenger}) => - canDeletePassenger({ - id: passenger.id, - attributes: {...passenger.attributes, travel: {data: travel}}, - }) && ( - <ListItemSecondaryAction> - <Button color="primary" onClick={onClick} tabIndex={-1}> - {t`travel.passengers.remove`} - </Button> - </ListItemSecondaryAction> - ) - } + Actions={({passenger}) => ( + <PassengerActions + passenger={passenger} + travel={travel} + setFocusPassenger={setFocusPassenger} + /> + )} /> + {focusPassenger && canSeePassengerDetails(focusPassenger) && ( + <DrawerPassenger + isOpen={!!focusPassenger} + onClose={() => setFocusPassenger(undefined)} + firstName={ + focusPassenger?.attributes.user?.data?.attributes.firstName + } + lastName={ + focusPassenger?.attributes.user?.data?.attributes.lastName + } + email={focusPassenger?.attributes.email} + phone={focusPassenger?.attributes.phone} + /> + )} </> )} </Paper>
M frontend/containers/Travel/useActions.tsxfrontend/containers/Travel/useActions.tsx

@@ -29,7 +29,7 @@ const [updatePassenger] = useUpdatePassengerMutation();

const [deletePassenger] = useDeletePassengerMutation(); const removePassengerFromTravel = async (passengerId: string) => { - const isCarosterPlus = event.enabled_modules.includes('caroster-plus'); + const isCarosterPlus = event.enabled_modules?.includes('caroster-plus'); if (isCarosterPlus) { try { await deletePassenger({
M frontend/containers/WaitingList/AssignButton.tsxfrontend/containers/WaitingList/AssignButton.tsx

@@ -7,20 +7,16 @@

interface Props { onClick: () => void; tabIndex?: number; - disabled: boolean; + disabled?: boolean; } const AssignButton = (props: Props) => { const {onClick, tabIndex} = props; const theme = useTheme(); - const {t} = useTranslation(); return ( - <ListItemSecondaryAction - onClick={onClick} - tabIndex={tabIndex} - > + <ListItemSecondaryAction onClick={onClick} tabIndex={tabIndex}> <IconButton sx={{ borderRadius: 1,
M frontend/containers/WaitingList/index.tsxfrontend/containers/WaitingList/index.tsx

@@ -19,7 +19,7 @@ import usePassengersActions from '../../hooks/usePassengersActions';

import RemoveDialog from '../RemoveDialog'; import AddPassengerButtons from '../AddPassengerButtons'; import AssignButton from './AssignButton'; -import PassengersList, { PassengerButton } from '../PassengersList'; +import PassengersList from '../PassengersList'; interface Props { canAddSelf: boolean;

@@ -37,10 +37,13 @@ const mobile = useMediaQuery(theme.breakpoints.down('md'));

const addToast = useToastStore(s => s.addToast); const [isEditing, toggleEditing] = useReducer(i => !i, false); const [removingPassenger, setRemovingPassenger] = useState(null); - const travels = - event?.travels?.data?.length > 0 - ? event?.travels?.data.slice().sort(sortTravels) - : []; + const travels = useMemo( + () => + event?.travels?.data?.length > 0 + ? event?.travels?.data.slice().sort(sortTravels) + : [], + [event?.travels?.data] + ); const {removePassenger} = usePassengersActions(); const availability = useMemo(() => {

@@ -52,9 +55,12 @@ return count + seats - passengers?.data?.length;

}, 0); }, [travels]); - const removePassengerCallback = useCallback(removePassenger, [event]); + const removePassengerCallback = useCallback(removePassenger, [ + event, + removePassenger, + ]); - const onPress = useCallback( + const onClickPassenger = useCallback( (passengerId: string) => { const selectedPassenger = event?.waitingPassengers?.data.find( item => item.id === passengerId

@@ -75,18 +81,6 @@ addToast(t('passenger.errors.cant_remove_passenger'));

} }; - const ListButton: PassengerButton = isEditing - ? ({onClick}) => ( - <ListItemSecondaryAction> - <IconButton size="small" color="primary" onClick={onClick}> - <CancelOutlinedIcon /> - </IconButton> - </ListItemSecondaryAction> - ) - : ({onClick, disabled}) => ( - <AssignButton onClick={onClick} tabIndex={-1} disabled={disabled} /> - ); - return ( <Container maxWidth="sm" sx={{mt: 11, mx: 0, px: mobile ? 2 : 4}}> <Paper sx={{width: '480px', maxWidth: '100%', position: 'relative'}}>

@@ -122,8 +116,21 @@ <Divider />

{event?.waitingPassengers?.data?.length > 0 && ( <PassengersList passengers={event.waitingPassengers.data} - onPress={onPress} - Button={ListButton} + onClickPassenger={onClickPassenger} + Actions={({passenger}) => + isEditing ? ( + <ListItemSecondaryAction> + <IconButton size="small" color="primary"> + <CancelOutlinedIcon /> + </IconButton> + </ListItemSecondaryAction> + ) : ( + <AssignButton + tabIndex={-1} + onClick={() => onClickPassenger(passenger.id)} + /> + ) + } /> )} </Paper>
M frontend/hooks/usePermissions.tsfrontend/hooks/usePermissions.ts

@@ -64,8 +64,10 @@ },

canJoinTravels: () => true, canAddTravel: () => true, canDeletePassenger: passenger => { - const travel = event?.travels?.data?.find( - travel => travel?.id === passenger.attributes.travel.data?.id + const travel = event?.travels?.data?.find(travel => + travel.attributes.passengers.data.some( + travelPassenger => travelPassenger.id === passenger.id + ) ); const isTravelCreator = travel?.attributes.user?.data?.id === userId; const isCurrentPassenger =

@@ -73,11 +75,15 @@ passenger.attributes.user?.data?.id === userId;

return isTravelCreator || isCurrentPassenger; }, canSeePassengerDetails: passenger => { - const travel = event?.travels?.data?.find( - travel => travel?.id === passenger.attributes.travel.data?.id + const travel = event?.travels?.data?.find(travel => + travel.attributes.passengers.data.some( + travelPassenger => travelPassenger.id === passenger.id + ) ); const isTravelCreator = travel?.attributes.user?.data?.id === userId; - return isTravelCreator; + const isCurrentPassenger = + passenger?.attributes.user?.data?.id === userId; + return isTravelCreator || isCurrentPassenger; }, }; return {userPermissions: carosterPlusPermissions};

@@ -88,6 +94,8 @@ else

return { userPermissions: { ...allPermissions, + canSeePassengerDetails: () => false, + canDeletePassenger: () => true, canEditEventOptions: () => userIsEventCreator, canJoinTravels: () => connected, },
M frontend/locales/en.jsonfrontend/locales/en.json

@@ -8,6 +8,7 @@ "alert.location.label": "Your location",

"alert.radius.label": "Radius desired", "alert.optional": "Optional", "alert.button.label": "Save", + "alert.description": "Set up an alert to receive an email in case of a nearby departure", "confirm.creating": "Creating the account", "confirm.google.title": "Complete registration", "confirm.text": "You have received an email with a link. Please click on this link to confirm your account.",

@@ -155,6 +156,7 @@ "passenger.informations.surname.label": "Surname",

"passenger.informations.phone.label": "Phone", "passenger.informations.email.label": "E-mail", "passenger.informations.call.label": "Call", + "passenger.informations.notSpecify": "Don't Specify", "passenger.success.added_self_to_car": "You have been added to this car", "passenger.success.added_self_to_waitlist": "You have been added to the waitlist. You'll be notified when new cars will be added.", "passenger.success.added_to_car": "{{name}} has been added to this car",

@@ -185,7 +187,7 @@ "profile.not_defined": "Not specified",

"profile.password_changed": "Password updated", "profile.title": "Profile", "profile.stripe_link.title": "Billing", - "profile.stripe_link.button": "My Stripe account", + "profile.stripe_link.button": "Historic", "signin.errors.CredentialsSignin": "Check your email and password. If your account is linked to Google, please use login with Google.", "signin.errors.EmailNotConfirmed": "Your account has not been confirmed. Please check your emails", "signin.email": "Email",
M frontend/locales/fr.jsonfrontend/locales/fr.json

@@ -8,6 +8,7 @@ "alert.location.label": "Votre localisation",

"alert.radius.label": "Rayon désiré", "alert.optional": "Optionnel", "alert.button.label": "Enregistrer", + "alert.description": "Configurez une alerte pour recevoir un email en cas de départ proche.", "confirm.creating": "Création de compte", "confirm.google.title": "Finaliser l'inscription", "confirm.text": "Vous avez reçu un email avec un lien. Merci de cliquer sur ce lien pour confirmer votre compte.",

@@ -155,6 +156,7 @@ "passenger.informations.surname.label": "Nom",

"passenger.informations.phone.label": "TĂ©lĂ©phone", "passenger.informations.email.label": "E-mail", "passenger.informations.call.label": "Appeler", + "passenger.informations.notSpecify": "Non prĂ©cisĂ©", "passenger.success.added_self_to_car": "Vous avez Ă©tĂ© ajoutĂ© Ă  la voiture", "passenger.success.added_self_to_waitlist": "Vous avez Ă©tĂ© ajoutĂ© Ă  la liste d’attente. Vous serez notifiĂ© Ă  l’ajout de nouvelles voitures", "passenger.success.added_to_car": "{{name}} a Ă©tĂ© ajoutĂ© Ă  la voiture",

@@ -185,7 +187,7 @@ "profile.not_defined": "Non précisé",

"profile.password_changed": "Mot de passe mis à jour", "profile.title": "Profil", "profile.stripe_link.title": "Facturation", - "profile.stripe_link.button": "Mon compte Stripe", + "profile.stripe_link.button": "Historique", "signin.errors.CredentialsSignin": "Vérifiez votre email et mot de passe. Si votre compte est lié à Google, merci d'utiliser l'authentification Google.", "signin.errors.EmailNotConfirmed": "Votre compte n'a pas été confirmé. Merci de vérifier vos emails", "signin.email": "Email",
M frontend/pages/e/[uuid]/alerts.tsxfrontend/pages/e/[uuid]/alerts.tsx

@@ -56,7 +56,7 @@ };

} const isCarosterPlus = - event?.attributes?.enabled_modules.includes('caroster-plus'); + event?.attributes?.enabled_modules?.includes('caroster-plus'); if (!isCarosterPlus) return { notFound: true,
M frontend/pages/e/[uuid]/waitingList.tsxfrontend/pages/e/[uuid]/waitingList.tsx

@@ -71,7 +71,7 @@ };

} const isCarosterPlus = - event?.attributes?.enabled_modules.includes('caroster-plus'); + event?.attributes?.enabled_modules?.includes('caroster-plus'); if (isCarosterPlus) return { notFound: true,