all repos — caroster @ 1bea64d2ff9f4e00589034a0c18c9204afb83c27

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

feat: ✨ Enable Caroster without Mapbox token
#440
Simon Mulquin simon@octree.ch
Fri, 15 Dec 2023 16:08:40 +0000
commit

1bea64d2ff9f4e00589034a0c18c9204afb83c27

parent

fe359e3d0bd28293a74106f4ce391f5127bce55f

M frontend/containers/CreateEvent/Step2.tsxfrontend/containers/CreateEvent/Step2.tsx

@@ -57,10 +57,12 @@ <PlaceInput

label={t('event.creation.address')} textFieldProps={{sx: {mt: 2}}} place={address} - onSelect={({location, place}) => { - setAddress(place); - setLatitude(location[1]); - setLongitude(location[0]); + latitude={event.latitude} + longitude={event.longitude} + onSelect={({place, latitude, longitude}) => { + setAddress(place); + setLatitude(latitude); + setLongitude(longitude); }} /> <TextField
M frontend/containers/NewTravelDialog/index.tsxfrontend/containers/NewTravelDialog/index.tsx

@@ -214,10 +214,12 @@ size: 'small',

sx: {...addSpacing(theme, 1), paddingBottom: theme.spacing(1)}, }} place={meeting} - onSelect={({location, place}) => { + latitude={meeting_latitude} + longitude={meeting_longitude} + onSelect={({place, latitude, longitude}) => { setMeeting(place); - setMeetingLatitude(location[1]); - setMeetingLongitude(location[0]); + setMeetingLatitude(latitude); + setMeetingLongitude(longitude); }} /> <TextField
M frontend/containers/PlaceInput/index.tsxfrontend/containers/PlaceInput/index.tsx

@@ -10,43 +10,108 @@ import getPlacesSuggestions from '../../lib/getPlacesSuggestion';

interface Props { place: string; + latitude?: number; + longitude?: number; onSelect: ({ - location, + latitude, + longitude, place, }: { - location: [number, number]; + latitude?: number; + longitude?: number; place: string; }) => void; label?: string; textFieldProps?: TextFieldProps; } +const MAPBOX_CONFIGURED = process.env['MAPBOX_CONFIGURED'] || false; + const PlaceInput = ({ place = '', + latitude, + longitude, onSelect, label, textFieldProps, }: Props) => { const {t} = useTranslation(); const {locale} = useLocale(); + const [mapboxAvailable, setMapboxAvailable] = useState(MAPBOX_CONFIGURED); + const [noCoordinates, setNoCoordinates] = useState(!latitude || !longitude); + const previousOption = place ? {place_name: place, previous: true} : null; const [options, setOptions] = useState([] as Array<any>); - const onChange = async (e, selectedOption) => { - onSelect({ - place: selectedOption.place_name, - location: selectedOption.center, - }); + if (selectedOption.previous) { + setNoCoordinates(!latitude || !longitude); + onSelect({ + place, + latitude, + longitude, + }); + } else if (selectedOption.center) { + const [optionLongitude, optionLatitude] = selectedOption.center; + setNoCoordinates(false); + onSelect({ + place: selectedOption.place_name, + latitude: optionLatitude, + longitude: optionLongitude, + }); + } else { + setNoCoordinates(true); + onSelect({ + place: selectedOption.place_name, + latitude: null, + longitude: null, + }); + } }; const updateOptions = debounce(async (e, search: string) => { if (search !== '') { - getPlacesSuggestions({search, proximity: 'ip', locale}).then(suggestions => { - setOptions(suggestions); - }); + getPlacesSuggestions({search, proximity: 'ip', locale}).then( + suggestions => { + let defaultOptions = []; + if (previousOption) { + defaultOptions = [previousOption]; + } + if (search && search !== previousOption?.place_name) { + defaultOptions = [...defaultOptions, {place_name: search}]; + } + if (suggestions?.length >= 1) { + setMapboxAvailable(true); + const [firstSuggestion, ...otherSuggestions] = suggestions; + let uniqueOptions = [...defaultOptions, ...otherSuggestions]; + if ( + firstSuggestion.place_name !== search || + firstSuggestion.place_name !== previousOption?.place_name + ) + uniqueOptions = [ + ...defaultOptions, + firstSuggestion, + ...otherSuggestions, + ]; + setOptions(uniqueOptions); + } else { + setMapboxAvailable(false); + setOptions(defaultOptions); + } + } + ); } }, 400); + const getHelperText = () => { + if (!mapboxAvailable) { + return t`placeInput.mapboxUnavailable`; + } + if (noCoordinates) { + return t`placeInput.noCoordinates`; + } + return null; + }; + return ( <Autocomplete freeSolo

@@ -54,7 +119,7 @@ disableClearable

getOptionLabel={option => option.place_name} options={options} autoComplete - defaultValue={{place_name: place}} + defaultValue={previousOption} filterOptions={x => x} noOptionsText={t('autocomplete.noMatch')} onChange={onChange}

@@ -64,6 +129,8 @@ <TextField

label={label} multiline maxRows={4} + helperText={MAPBOX_CONFIGURED && getHelperText()} + FormHelperTextProps={{sx: {color: 'warning.main'}}} InputProps={{ endAdornment: ( <InputAdornment position="end" sx={{mr: -0.5}}>
M frontend/containers/Travel/HeaderEditing.tsxfrontend/containers/Travel/HeaderEditing.tsx

@@ -116,15 +116,17 @@ name="phone"

id="EditTravelPhone" /> <PlaceInput - label={t('travel.creation.meeting')} - textFieldProps={{sx: {pb: 2}}} - place={meeting} - onSelect={({location, place}) => { - setMeeting(place); - setMeetingLatitude(location[1]); - setMeetingLongitude(location[0]); - }} - /> + label={t('travel.creation.meeting')} + textFieldProps={{sx: {pb: 2}}} + place={meeting} + latitude={meeting_latitude} + longitude={meeting_longitude} + onSelect={({place, latitude, longitude}) => { + setMeeting(place); + setMeetingLatitude(latitude); + setMeetingLongitude(longitude); + }} + /> <TextField label={t('travel.creation.notes')} fullWidth
M frontend/containers/TravelColumns/index.tsxfrontend/containers/TravelColumns/index.tsx

@@ -69,7 +69,12 @@ />

); const {latitude, longitude} = event; - const showMap = latitude && longitude; + const showMap = + (latitude && longitude) || + travels.some( + ({attributes: {meeting_latitude, meeting_longitude}}) => + meeting_latitude && meeting_longitude + ); let coordsString = `${latitude}${longitude}`; const markers = travels.reduce((markers, travel) => { const {

@@ -93,15 +98,15 @@

const mapUpdateKey = `${event.uuid}.travels+${coordsString}`; if (preventUpdateKey !== mapUpdateKey) { setPreventUpdateKey(mapUpdateKey); - setCenter([latitude, longitude]); - setMarkers([ - { + if (latitude && longitude) { + setCenter([latitude, longitude]); + markers.push({ double: true, center: [latitude, longitude], popup: <EventPopup event={event} />, - }, - ...markers, - ]); + }); + } + setMarkers(markers); } return (
M frontend/locales/en.jsonfrontend/locales/en.json

@@ -116,6 +116,8 @@ "passenger.success.added_to_car": "{{name}} has been added to this car",

"passenger.success.added_to_waitlist": "{{name}} has been added to the waitlist", "passenger.success.goToTravels": "Go to travels", "passenger.title": "Waiting list", + "placeInput.mapboxUnavailable": "We are not able to suggest geolocated places at the moment", + "placeInput.noCoordinates": "This place is not geo-located and won't appear on the map", "profile.actions.cancel": "Cancel", "profile.actions.change_password": "Change your password", "profile.actions.edit": "Edit",
M frontend/locales/fr.jsonfrontend/locales/fr.json

@@ -116,6 +116,8 @@ "passenger.success.added_to_car": "{{name}} a été ajouté à la voiture",

"passenger.success.added_to_waitlist": "{{name}} ajouté à la liste d'attente", "passenger.success.goToTravels": "Aller aux trajets", "passenger.title": "Liste d'attente", + "placeInput.mapboxUnavailable": "Nous ne pouvons pas suggérer de lieux géolocalisés pour le moment", + "placeInput.noCoordinates": "Le lieu ne peut être géo-localisé actuellement et ne sera pas affiché sur la carte.", "profile.actions.cancel": "Annuler", "profile.actions.change_password": "Changer son mot de passe", "profile.actions.edit": "Editer",
M frontend/locales/nl.jsonfrontend/locales/nl.json

@@ -116,6 +116,8 @@ "passenger.success.added_to_car": "{{name}} is toegevoegd aan deze auto",

"passenger.success.added_to_waitlist": "{{name}} is toegevoegd aan de wachtlijst", "passenger.success.goToTravels": "Ga naar reizen", "passenger.title": "Wachtlijst", + "placeInput.mapboxUnavailable": "", + "placeInput.noCoordinates": "", "profile.actions.cancel": "Annuleren", "profile.actions.change_password": "Wachtwoord wijzigen", "profile.actions.edit": "Bewerken",
M frontend/locales/pl.jsonfrontend/locales/pl.json

@@ -120,6 +120,8 @@ "passenger.success.added_to_car": "Dodano {{name}} do tego samochodu",

"passenger.success.added_to_waitlist": "Dodano {{name}} do listy oczekujących", "passenger.success.goToTravels": "", "passenger.title": "", + "placeInput.mapboxUnavailable": "", + "placeInput.noCoordinates": "", "profile.actions.cancel": "", "profile.actions.change_password": "", "profile.actions.edit": "",
M frontend/locales/sv.jsonfrontend/locales/sv.json

@@ -116,6 +116,8 @@ "passenger.success.added_to_car": "",

"passenger.success.added_to_waitlist": "", "passenger.success.goToTravels": "", "passenger.title": "", + "placeInput.mapboxUnavailable": "", + "placeInput.noCoordinates": "", "profile.actions.cancel": "", "profile.actions.change_password": "", "profile.actions.edit": "",
M frontend/next.config.jsfrontend/next.config.js

@@ -1,5 +1,12 @@

const {i18n} = require('./react-i18next.config'); -const {NODE_ENV, DEV_TILES_URL, STRAPI_URL = 'http://localhost:1337', DEFAULT_LOCALE = 'share'} = process.env; +const { + NODE_ENV, + DEV_TILES_URL, + STRAPI_URL = 'http://localhost:1337', + DEFAULT_LOCALE = 'share', + MAPBOX_TOKEN, + MAPBOX_URL, +} = process.env; const withPWA = require('next-pwa')({ dest: 'public',

@@ -17,7 +24,8 @@ },

env: { STRAPI_URL, DEV_TILES_URL, - DEFAULT_LOCALE + DEFAULT_LOCALE, + MAPBOX_CONFIGURED: !!MAPBOX_TOKEN && !!MAPBOX_URL, }, i18n,
M frontend/pages/api/mapbox/places.tsfrontend/pages/api/mapbox/places.ts

@@ -9,22 +9,25 @@ req: NextApiRequest,

res: NextApiResponse<ResponseData> ) { const {search, proximity = 'ip', locale} = req.query; - if (!search) return res.status(400); - if (!MAPBOX_TOKEN) return res.status(500); + if (!search) return res.status(400).send(null); + else if (!MAPBOX_TOKEN) return res.status(500).send(null); const url = `${MAPBOX_URL}geocoding/v5/mapbox.places/${search}.json?proximity=${proximity}&access_token=${MAPBOX_TOKEN}&language=${locale}`; const mapBoxResult = await fetch(url) - .then(response => response.json()) + .then(response => { + if (response.status === 429) { + throw 'MAPBOX_RATE_LIMIT_EXCEEDED'; + } + return response.json(); + }) .catch(error => { console.error(error); - res.send(500); }); if (mapBoxResult?.features) { - res.send(mapBoxResult.features); - res.status(200); + res.status(200).send(mapBoxResult.features); + return; } - - return; + res.status(500).send(null); }
M frontend/pages/e/[uuid]/details.tsxfrontend/pages/e/[uuid]/details.tsx

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

import moment from 'moment'; import Tooltip from '@mui/material/Tooltip'; import IconButton from '@mui/material/IconButton'; +import Button from '@mui/material/Button'; import Box from '@mui/material/Box'; import Link from '@mui/material/Link'; import Card from '@mui/material/Card';

@@ -43,6 +44,8 @@ const addToast = useToastStore(s => s.addToast);

const setEventUpdate = useEventStore(s => s.setEventUpdate); const event = useEventStore(s => s.event); const [isEditing, setIsEditing] = useState(false); + + if (!event) return null; const onSave = async e => { try {

@@ -91,8 +94,6 @@ <TuneIcon />

</IconButton> </Tooltip> ); - - if (!event) return null; return ( <Box

@@ -189,13 +190,15 @@ </Typography>

{isEditing ? ( <PlaceInput place={event.address} - onSelect={({location, place}) => { + latitude={event.latitude} + longitude={event.longitude} + onSelect={({place, latitude, longitude}) => setEventUpdate({ address: place, - latitude: location[1], - longitude: location[0], - }); - }} + latitude, + longitude, + }) + } /> ) : ( <Box position="relative">