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 */}
+
+ >
+ );
+}
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 */}
+
+ >
+ );
+}
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 */}
+
+ >
+ );
+}
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 */}
+
+ >
+ );
+}
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!
+
+
+
+
+
+ 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"