diff --git a/web-app/package.json b/web-app/package.json index 5399ff258..dc2951d9c 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -30,6 +30,7 @@ "react-dom": "^17.0.0 || ^18.0.0", "react-ga4": "^2.1.0", "react-google-recaptcha": "^3.1.0", + "react-hook-form": "^7.52.0", "react-i18next": "^14.1.2", "react-leaflet": "^4.2.1", "react-loading-overlay-ts": "^2.0.2", diff --git a/web-app/public/assets/rocket.gif b/web-app/public/assets/rocket.gif new file mode 100644 index 000000000..ef23445fb Binary files /dev/null and b/web-app/public/assets/rocket.gif differ diff --git a/web-app/src/app/Theme.ts b/web-app/src/app/Theme.ts index e12dace90..a50dea3bf 100644 --- a/web-app/src/app/Theme.ts +++ b/web-app/src/app/Theme.ts @@ -30,7 +30,7 @@ const palette = { }, text: { primary: '#474747', - secondary: '#95a4f4', + secondary: 'rgba(71, 71, 71, 0.8)', disabled: 'rgba(0,0,0,0.3)', }, }; @@ -50,10 +50,35 @@ export const theme = createTheme({ fontFamily: '"Muli"', }, components: { + MuiFormLabel: { + styleOverrides: { + root: { + color: '#474747', + fontWeight: 'bold', + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + '&.md-small-input': { + input: { paddingTop: '7px', paddingBottom: '7px' }, + }, + }, + }, + }, + MuiSelect: { + styleOverrides: { + root: { + '.MuiSelect-select': { paddingTop: '7px', paddingBottom: '7px' }, + }, + }, + }, MuiButton: { styleOverrides: { root: { textTransform: 'none', + boxShadow: 'none', }, }, }, diff --git a/web-app/src/app/router/Router.tsx b/web-app/src/app/router/Router.tsx index 7beae4113..3ce201191 100644 --- a/web-app/src/app/router/Router.tsx +++ b/web-app/src/app/router/Router.tsx @@ -11,7 +11,6 @@ import Home from '../screens/Home'; import ForgotPassword from '../screens/ForgotPassword'; import FAQ from '../screens/FAQ'; import About from '../screens/About'; -import Contribute from '../screens/Contribute'; import PostRegistration from '../screens/PostRegistration'; import TermsAndConditions from '../screens/TermsAndConditions'; import PrivacyPolicy from '../screens/PrivacyPolicy'; @@ -25,6 +24,9 @@ import { } from '../services/channel-service'; import { useAppDispatch } from '../hooks'; import { logout } from '../store/profile-reducer'; +import FeedSubmission from '../screens/FeedSubmission'; +import FeedSubmissionFAQ from '../screens/FeedSubmissionFAQ'; +import FeedSubmitted from '../screens/FeedSubmitted'; export const AppRouter: React.FC = () => { const navigateTo = useNavigate(); @@ -82,7 +84,9 @@ export const AppRouter: React.FC = () => { } /> } /> } /> - } /> + } /> + } /> + } /> } /> } /> diff --git a/web-app/src/app/screens/FeedSubmission/FeedSubmissionStepper.tsx b/web-app/src/app/screens/FeedSubmission/FeedSubmissionStepper.tsx new file mode 100644 index 000000000..0eacc8d9a --- /dev/null +++ b/web-app/src/app/screens/FeedSubmission/FeedSubmissionStepper.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stepper from '@mui/material/Stepper'; +import Step from '@mui/material/Step'; +import StepLabel from '@mui/material/StepLabel'; +import FeedSubmissionForm from './Form'; +import { useNavigate } from 'react-router-dom'; + +const steps = ['', '', '']; + +export default function FeedSubmissionStepper(): React.ReactElement { + const [activeStep, setActiveStep] = React.useState(0); + const navigateTo = useNavigate(); + + const handleNext = (): void => { + const nextStep = activeStep + 1; + setActiveStep(nextStep); + if (nextStep === steps.length) { + navigateTo('/contribute/submitted'); + } + }; + + const handleBack = (): void => { + setActiveStep((prevActiveStep) => prevActiveStep - 1); + }; + + return ( + + + {steps.map((label) => { + const stepProps: { completed?: boolean } = {}; + const labelProps: { + optional?: React.ReactNode; + } = {}; + return ( + + {label} + + ); + })} + + + + + ); +} diff --git a/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx new file mode 100644 index 000000000..29a97824d --- /dev/null +++ b/web-app/src/app/screens/FeedSubmission/Form/FirstStep.tsx @@ -0,0 +1,201 @@ +import { + Grid, + FormControl, + FormLabel, + RadioGroup, + FormControlLabel, + Radio, + Select, + MenuItem, + Button, + TextField, + FormHelperText, +} from '@mui/material'; + +import { type SubmitHandler, Controller, useForm } from 'react-hook-form'; +import { type FeedSubmissionFormFormInput } from '.'; + +export interface FeedSubmissionFormFormInputFirstStep { + name: string; + isOfficialProducer: string; + dataType: string; + transitProviderName: string; + feedLink: string; + licensePath: string; +} + +interface FormFirstStepProps { + initialValues: FeedSubmissionFormFormInput; + submitFormData: (formData: Partial) => void; +} + +export default function FormFirstStep({ + initialValues, + submitFormData, +}: FormFirstStepProps): React.ReactElement { + const { + control, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + name: initialValues.name, + isOfficialProducer: initialValues.isOfficialProducer, + dataType: initialValues.dataType, + transitProviderName: initialValues.transitProviderName, + feedLink: initialValues.feedLink, + licensePath: initialValues.licensePath, + }, + }); + const onSubmit: SubmitHandler = ( + data, + ): void => { + submitFormData(data); + }; + return ( + <> + {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */} +
+ + + + + Your Name and (if applicable) Organization + + ( + + )} + /> + + + + + + Are you the official producer or transit agency responsible for + this data ? + + ( + <> + + } + label='Yes' + /> + } + label='No' + /> + + + {errors.isOfficialProducer?.message ?? ''} + + + )} + /> + + + + + Data Type + ( + + )} + /> + + + + + Transit Provider Name + ( + + )} + /> + + + + + + Feed link + + ( + + )} + /> + + + + + Link to feed license + ( + + )} + /> + + + + + + + + + +
+ + ); +} diff --git a/web-app/src/app/screens/FeedSubmission/Form/SecondStep.tsx b/web-app/src/app/screens/FeedSubmission/Form/SecondStep.tsx new file mode 100644 index 000000000..5f77f109b --- /dev/null +++ b/web-app/src/app/screens/FeedSubmission/Form/SecondStep.tsx @@ -0,0 +1,166 @@ +import { + Typography, + Grid, + FormControl, + FormLabel, + RadioGroup, + FormControlLabel, + Radio, + Button, + MenuItem, + Select, + TextField, + FormHelperText, +} from '@mui/material'; +import { Controller, type SubmitHandler, useForm } from 'react-hook-form'; +import { type FeedSubmissionFormFormInput } from '.'; + +export interface FeedSubmissionFormInputSecondStep { + country: string; + region: string; + municipality: string; + isAuthRequired: string; +} + +interface FormSecondStepProps { + initialValues: FeedSubmissionFormFormInput; + submitFormData: (formData: Partial) => void; + handleBack: (formData: Partial) => void; +} + +export default function FormSecondStep({ + initialValues, + submitFormData, + handleBack, +}: FormSecondStepProps): React.ReactElement { + const { + control, + handleSubmit, + formState: { errors }, + getValues, + } = useForm({ + defaultValues: { + country: initialValues.country, + region: initialValues.region, + municipality: initialValues.municipality, + isAuthRequired: initialValues.isAuthRequired, + }, + }); + const onSubmit: SubmitHandler = (data) => { + submitFormData(data); + }; + + return ( + <> + GTFS Schedule Feed + {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */} +
+ + + + + Country + + ( + <> + + + {errors.country?.message ?? ''} + + + )} + /> + + + + + Region + ( + + )} + /> + + + + + Municipality + ( + + )} + /> + + + + + + Is authentication required? + + ( + + } + label='Yes' + /> + } + label='No' + /> + + )} + /> + + + + + + + + + + + + +
+ + ); +} diff --git a/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx new file mode 100644 index 000000000..a7ba7754d --- /dev/null +++ b/web-app/src/app/screens/FeedSubmission/Form/SecondStepRealtime.tsx @@ -0,0 +1,195 @@ +import { + Typography, + Grid, + FormControl, + FormLabel, + FormControlLabel, + Checkbox, + RadioGroup, + Radio, + Button, + TextField, +} from '@mui/material'; +import { type SubmitHandler, Controller, useForm } from 'react-hook-form'; +import { type FeedSubmissionFormFormInput } from '.'; + +export interface FeedSubmissionFormInputSecondStepRT { + tripUpdates: boolean; + vehiclePositions: boolean; + serviceAlerts: boolean; + gtfsRealtimeLink: string; + gtfsRelatedScheduleLink: string; + note: string; + isAuthRequired: string; +} + +interface FormSecondStepRTProps { + initialValues: FeedSubmissionFormFormInput; + submitFormData: (formData: Partial) => void; + handleBack: (formData: Partial) => void; +} + +export default function FormSecondStepRT({ + initialValues, + submitFormData, + handleBack, +}: FormSecondStepRTProps): React.ReactElement { + const { + control, + handleSubmit, + formState: { errors }, + getValues, + } = useForm({ + defaultValues: { + tripUpdates: initialValues.tripUpdates, + vehiclePositions: initialValues.vehiclePositions, + serviceAlerts: initialValues.serviceAlerts, + gtfsRealtimeLink: initialValues.gtfsRealtimeLink, + gtfsRelatedScheduleLink: initialValues.gtfsRelatedScheduleLink, + note: initialValues.note, + isAuthRequired: initialValues.isAuthRequired, + }, + }); + + const onSubmit: SubmitHandler = ( + data, + ) => { + submitFormData(data); + }; + + const entityTypeCheckBoxLabels = { + tripUpdates: 'Trip Updates', + vehiclePositions: 'Vehicle Positions', + serviceAlerts: 'Service Alerts', + }; + + return ( + <> + + GTFS Realtime Feed + + {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */} +
+ + + + Entity Type + {( + ['tripUpdates', 'vehiclePositions', 'serviceAlerts'] as const + ).map((entityType) => ( + { + return ( + } + label={entityTypeCheckBoxLabels[entityType]} + /> + ); + }} + /> + ))} + + + + + GTFS Realtime feed link + ( + + )} + /> + + + + + + Link to related GTFS Schedule feed + + ( + + )} + /> + + + + + Note + ( + + )} + /> + + + + + + Is authentication required? + + ( + + } + label='Yes' + /> + } + label='No' + /> + + )} + /> + + + + + + + + + + + +
+ + ); +} diff --git a/web-app/src/app/screens/FeedSubmission/Form/ThirdStep.tsx b/web-app/src/app/screens/FeedSubmission/Form/ThirdStep.tsx new file mode 100644 index 000000000..9067fc7f8 --- /dev/null +++ b/web-app/src/app/screens/FeedSubmission/Form/ThirdStep.tsx @@ -0,0 +1,174 @@ +import { + Typography, + Grid, + FormControl, + FormLabel, + Button, + TextField, + MenuItem, + Select, +} from '@mui/material'; +import { type SubmitHandler, Controller, useForm } from 'react-hook-form'; +import { type FeedSubmissionFormFormInput } from '.'; + +export interface FeedSubmissionFormInputThirdStep { + dataProducerEmail: string; + isInterestedInQualityAudit: boolean; + whatToolsUsedText: string; +} + +interface FormSecondStepRTProps { + initialValues: FeedSubmissionFormFormInput; + submitFormData: (formData: Partial) => void; + handleBack: (formData: Partial) => void; +} + +export default function FormThirdStep({ + initialValues, + submitFormData, + handleBack, +}: FormSecondStepRTProps): React.ReactElement { + const { + control, + handleSubmit, + formState: { errors }, + getValues, + } = useForm({ + defaultValues: { + dataProducerEmail: initialValues.dataProducerEmail, + isInterestedInQualityAudit: initialValues.isInterestedInQualityAudit, + whatToolsUsedText: initialValues.whatToolsUsedText, + }, + }); + const onSubmit: SubmitHandler = (data) => { + submitFormData(data); + }; + return ( + <> + {/* eslint-disable-next-line @typescript-eslint/no-misused-promises */} +
+ + + + + Data Producer Email

+ + This is an official email that consumers of the feed can + contact to ask questions. + +
+ ( + + )} + /> +
+
+ + {/* TODO: UX design decisionz: dropdown or radio buttons? */} + {/* + + Are you interested in a data quality audit? + + This is a 1 time meeting with MobilityData to review your + GTFS validation report and discuss possible improvements. + + + ( + + } + label='Yes' + /> + } + label='No' + /> + + )} + /> + */} + + + Are you interested in a data quality audit? +

+ + This is a 1 time meeting with MobilityData to review your GTFS + validation report and discuss possible improvements. + +
+ ( + + )} + /> +
+
+ + + + What tools do you use to create GTFS data? Could include open + source libraries, vendor services, or other applications. + + ( + + )} + /> + + + + + + + + + + + +
+
+ + ); +} diff --git a/web-app/src/app/screens/FeedSubmission/Form/index.tsx b/web-app/src/app/screens/FeedSubmission/Form/index.tsx new file mode 100644 index 000000000..6357e642f --- /dev/null +++ b/web-app/src/app/screens/FeedSubmission/Form/index.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import FormFirstStep from './FirstStep'; +import FormSecondStep from './SecondStep'; +import FormSecondStepRT from './SecondStepRealtime'; +import FormThirdStep from './ThirdStep'; + +export interface FeedSubmissionFormProps { + activeStep: number; + handleBack: () => void; + handleNext: () => void; +} + +export interface FeedSubmissionFormFormInput { + name: string; + isOfficialProducer: string; + dataType: string; + transitProviderName: string; + feedLink: string; + licensePath: string; + country: string; + region: string; + municipality: string; + tripUpdates: boolean; + vehiclePositions: boolean; + serviceAlerts: boolean; + gtfsRealtimeLink: string; + gtfsRelatedScheduleLink: string; + note: string; + isAuthRequired: string; + dataProducerEmail: string; + isInterestedInQualityAudit: boolean; + whatToolsUsedText: string; +} + +const defaultFormValues: FeedSubmissionFormFormInput = { + name: '', + isOfficialProducer: '', + dataType: 'GTFS Schedule', + transitProviderName: '', + feedLink: '', + licensePath: '', + country: '', + region: '', + municipality: '', + tripUpdates: false, + vehiclePositions: false, + serviceAlerts: false, + gtfsRealtimeLink: '', + gtfsRelatedScheduleLink: '', + note: '', + isAuthRequired: 'no', + dataProducerEmail: '', + isInterestedInQualityAudit: false, + whatToolsUsedText: '', +}; + +export default function FeedSubmissionForm({ + activeStep, + handleNext, + handleBack, +}: FeedSubmissionFormProps): React.ReactElement { + const [formData, setFormData] = + React.useState(defaultFormValues); + + const formStepSubmit = ( + partialFormData: Partial, + ): void => { + setFormData((prevData) => ({ ...prevData, ...partialFormData })); + handleNext(); + }; + + const formStepBack = ( + partialFormData: Partial, + ): void => { + setFormData((prevData) => ({ ...prevData, ...partialFormData })); + handleBack(); + }; + + const finalSubmit = ( + partialFormData: Partial, + ): void => { + const finalData = { ...formData, ...partialFormData }; + setFormData(finalData); + console.log('FINAL API CALL WITH', finalData); + // TODO: API call with finalData + // TODO: loading state of API call + // TODO: feed submitted page + handleNext(); + }; + + return ( + <> + {activeStep === 0 && ( + + )} + {activeStep === 1 && formData.dataType === 'GTFS Schedule' && ( + + )} + {activeStep === 1 && formData.dataType === 'GTFS Realtime' && ( + + )} + {activeStep === 2 && ( + + )} + + ); +} diff --git a/web-app/src/app/screens/FeedSubmission/index.tsx b/web-app/src/app/screens/FeedSubmission/index.tsx new file mode 100644 index 000000000..24e58d76a --- /dev/null +++ b/web-app/src/app/screens/FeedSubmission/index.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { Box, Container, CssBaseline, Typography, colors } from '@mui/material'; +import { + selectIsAnonymous, + selectIsAuthenticated, + selectUserProfile, +} from '../../store/profile-selectors'; +import FeedSubmissionStepper from './FeedSubmissionStepper'; + +export default function FeedSubmission(): React.ReactElement { + const user = useSelector(selectUserProfile); + const isAuthenticatedOrAnonymous = + useSelector(selectIsAuthenticated) || useSelector(selectIsAnonymous); + + useEffect(() => { + if (isAuthenticatedOrAnonymous && user?.accessToken !== undefined) { + console.log('User is authenticated or anonymous'); + } + }, [isAuthenticatedOrAnonymous]); + + return ( + + + + + Do you have any questions about how to submit a feed?{' '} + + Read our FAQ + + + + + + Add or update a feed + + + + + ); +} diff --git a/web-app/src/app/screens/FeedSubmissionFAQ.tsx b/web-app/src/app/screens/FeedSubmissionFAQ.tsx new file mode 100644 index 000000000..bcc01dc40 --- /dev/null +++ b/web-app/src/app/screens/FeedSubmissionFAQ.tsx @@ -0,0 +1,407 @@ +import * as React from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + type SxProps, + Typography, + colors, + Box, + Container, + CssBaseline, +} from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; + +const accordionStyle: SxProps = { + boxShadow: 'none', + background: 'transparent', + borderBottom: '2px solid #13151A', + '&:before': { display: 'none' }, + svg: { color: '#13151A' }, +}; + +export default function FeedSubmissionFAQ(): React.ReactElement { + return ( + + + + + Frequently Asked Questions about Adding Feeds + + + } + > + + What is a GTFS feed? + + + + + A GTFS feed is a downloadable set of files that adhere to the{' '} + + General Transit Feed Specification + + . +
+
A GTFS Schedule feed that includes static information about + a transit service is a collection of text (.txt) files that are + contained in a single ZIP file. A GTFS Realtime feed that provides + realtime updates to riders is formatted as{' '} + + Protocol Buffer data + {' '} + and shared as a proto file. A GTFS Realtime feed can include a mix + of Trip Updates, Vehicle Positions, and Service Alerts or there + can be separate feeds for each type of realtime information. +
+
+ Each direct download URL for a GTFS feed has to open a file. For + example, a URL that points to an agency's GTFS explainer page + such as{' '} + + https://www.bctransit.com/open-data + {' '} + is not a valid GTFS feed URL. However,{' '} + + https://www.bctransit.com/data/gtfs/powell-river.zip + {' '} + is a valid GTFS feed download link because it directly opens the + GTFS feed. The same principle is used for GTFS feeds that are + accessible via an API: a generic link to the API, such as{' '} + + http://api.511.org/transit/datafeeds + + , is invalid. A valid download URL would need to include an API + query that returns a GTFS feed, such as{' '} + + http://api.511.org/transit/datafeeds?operator_id=3D + + . +
+
+
+ + + } + > + + Why would I want to add or update a feed? + + + + + Adding a feed means that more journey planning apps can discover + the data and share it with travelers. Service planning tools and + researchers also rely on data aggregators like the Mobility + Database catalogs to evaluate services and plan future ones. +
+
+ To ensure that travelers have access to the most up-to-date + information, transit providers should add a new feed on the + catalogs when their feed URL changes. Transit providers should + review{' '} + + the spreadsheet of feeds already in the Mobility Database + {' '} + to see if an old URL of their feed is in the Mobility Database and + request that its status be set to deprecated under Issue Type in + the form below. +
+
+ Deprecated is a manually set status within the Mobility + Database that indicates that a feed has been replaced with a new + URL. MobilityData staff will deprecate the old feed and set a{' '} + redirect to indicate that the new feed should be used + instead of the deprecated one. +
+
+ If transit providers would like to share old feed URLs for + researchers and analysts to use, please add the feed to the form + below and request that its status be set to deprecated. +
+
+
+ + + } + > + + When should I contribute a feed? + + + + + To ensure that travelers have access to the most up-to-date + information, transit providers should add a new feed on the + catalogs when there are major changes to their URL. Examples of + changes include: +
    +
  • The feed URL changes
  • +
  • + The feed is combined with several other feeds (for example: + several providers' feeds are combined together) +
  • +
  • + The feed is split from a combined/aggregated feed (for + example: a provider whose GTFS was only available in an + aggregate feed now has their own independent feed) +
  • +
+
+
+
+ + + } + > + + Who can contribute a feed? + + + + + Anyone can add or update a feed, and it is currently merged + manually into the catalogs by the MobilityData team. The name of + the person requesting the feed is captured in the PR, either via + their GitHub profile or based on the information shared in the + form below. +
+
+ In order to verify the validity of a GTFS schedule source, an + automated test is also run to check if the direct download URL + provided opens a functional ZIP file. +
+
+
+ + + } + > + + How do I contribute a feed? + + + + + There are two ways to update a feed: +
+
+ + 1. If you're not comfortable with GitHub or only have a few + feeds to add: + {' '} + use the form below to request a feed change. The feed will be + added as a pull request in GitHub viewable to the public within a + week of being submitted. You can verify the change has been made + to the catalogs by reviewing this CSV file. In the future, this + process will be automated so the PR is automatically created once + submitted and merged when tests pass. +
+
+ + 2. If you want to add feeds directly: + {' '} + you can follow{' '} + + the CONTRIBUTING.MD file + {' '} + in GitHub to add sources. +
+
+ If you have any questions or concerns about this process, you can + email{' '} + + api@mobilitydata.org + {' '} + for support in getting your feed added. +
+
+
+ + + } + > + + What if I want to remove a feed? + + + + + Feeds are only removed in instances when it is requested by the + producer of the data because of licensing issues. In all other + cases, feeds are set to a status of deprecated so it's + possible to include their historical data within the Mobility + Database. + + + + + + } + > + + Shoutout to our incredible contributors + + + + + 🎉 Thanks to all those who have contributed. This includes any + organizations or unaffiliated individuals who have added data, + updated data, or contributed code since 2021. +
+
+ Organizations: +
    +
  • Adelaide Metro
  • +
  • Bettendorf Transit
  • +
  • Bi-State Regional Commission
  • +
  • BreizhGo
  • +
  • Cal-ITP
  • +
  • Commerce Municipal Bus Lines
  • +
  • Corpus Christi Regional Transportation Authority
  • +
  • County of Hawai'i Mass Transit Agency
  • +
  • DART Delaware
  • +
  • + Department of Municipalities and Transport, Abu Dhabi, United + Arab Emirates +
  • +
  • Development Bank of Latin America (CAF)
  • +
  • Digital Transport for Africa (DT4A)
  • +
  • ECO Transit
  • +
  • Eismo Info
  • +
  • Entur AS
  • +
  • GTFS.be
  • +
  • Garnet Consultants
  • +
  • Golden Gate Bridge Highway Transit District
  • +
  • High Valley Transit
  • +
  • Kitsap Transit
  • +
  • Kuzzle
  • +
  • Metro Christchurch
  • +
  • Metro de Málaga
  • +
  • Passio Technologies
  • +
  • Pinpoint AVL
  • +
  • Port Phillip Ferries
  • +
  • Redmon Group
  • +
  • Rhode Island Public Transit Authority (RIPTA)
  • +
  • Rio de Janeiro City Hall
  • +
  • Rochester-Genesee Regional Transportation Authority
  • +
  • Roma Mobilita
  • +
  • SFMTA
  • +
  • SMMAG
  • +
  • San Francisco Municipal Transportation Agency (SFMTA)
  • +
  • San Luis Obispo Regional Transit Authority
  • +
  • Santiago Directorio de Transporte Público Metropolitano
  • +
  • Skedgo
  • +
  • Société nationale des chemins de fer français (SNCF)
  • +
  • Sound Transit
  • +
  • Springfield Mass Transit District (SMTD)
  • +
  • Ticpoi
  • +
  • Transcollines
  • +
  • Transport for Cairo
  • +
  • Two Sigma Data Clinic
  • +
  • UCSC Transporation and Parking Services
  • +
  • Unobus
  • +
  • Volánbusz
  • +
  • Walker Consultants
  • +
+
+
+ Individuals:
+ If you are listed here and would like to add your organization,{' '} + + let MobilityData know + + . +
    +
  • @1-Byte on GitHub
  • +
  • Allan Fernando
  • +
  • Eloi Torrents
  • +
  • Florian Maunier
  • +
  • Gábor Kovács
  • +
  • Jessica Rapson
  • +
  • Joop Kiefte
  • +
  • Justin Brooks
  • +
  • Kevin Butler
  • +
  • Kovács Áron
  • +
  • Oliver Hattshire
  • +
  • Saipraneeth Devunuri
  • +
+
+
+
+
+
+ ); +} diff --git a/web-app/src/app/screens/FeedSubmitted.tsx b/web-app/src/app/screens/FeedSubmitted.tsx new file mode 100644 index 000000000..e174168eb --- /dev/null +++ b/web-app/src/app/screens/FeedSubmitted.tsx @@ -0,0 +1,42 @@ +import { Typography, Box, colors, Container, CssBaseline } from '@mui/material'; + +export default function FeedSubmitted(): React.ReactElement { + return ( + + + + 🚀 Your feed has been submitted! + + + rocket + + + Thank you for your precious contribution to the Mobility Database! + Your feed will be available on the website within the next 2 weeks. + + + You’ll also be included in our {/* TODO: implement ancor */} + Contributors List. + + + If you have any questions or feedback,{' '} + please contact us. + + + + + ); +} diff --git a/web-app/yarn.lock b/web-app/yarn.lock index 2eaad7e8b..2956f5936 100644 --- a/web-app/yarn.lock +++ b/web-app/yarn.lock @@ -12047,6 +12047,11 @@ react-google-recaptcha@^3.1.0: prop-types "^15.5.0" react-async-script "^1.2.0" +react-hook-form@^7.52.0: + version "7.52.0" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.52.0.tgz#e52b33043e283719586b9dd80f6d51b68dd3999c" + integrity sha512-mJX506Xc6mirzLsmXUJyqlAI3Kj9Ph2RhplYhUVffeOQSnubK2uVqBFOBJmvKikvbFV91pxVXmDiR+QMF19x6A== + react-i18next@^14.1.2: version "14.1.2" resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-14.1.2.tgz#cd57a755f25a32a5fcc3dbe546cf3cc62b4f3ebd"