diff --git a/src/app/components/CoveredAreaMap.tsx b/src/app/components/CoveredAreaMap.tsx index bf95a55..82f57e4 100644 --- a/src/app/components/CoveredAreaMap.tsx +++ b/src/app/components/CoveredAreaMap.tsx @@ -31,12 +31,10 @@ import { import { OpenInNew } from '@mui/icons-material'; import { computeBoundingBox } from '../screens/Feed/Feed.functions'; import { displayFormattedDate } from '../utils/date'; -import { useSelector } from 'react-redux'; import ModeOfTravelIcon from '@mui/icons-material/ModeOfTravel'; import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap'; import { useRemoteConfig } from '../context/RemoteConfigProvider'; import { sendGAEvent } from '@next/third-parties/google'; -import { selectGtfsDatasetRoutesLoadingStatus } from '../store/supporting-files-selectors'; import { getLatestGbfsVersion, type LatestDatasetLite, @@ -111,9 +109,6 @@ const CoveredAreaMap: React.FC = ({ return getLatestGbfsVersion(feed as GBFSFeedType); }, [feed]); - const routesJsonLoadingStatus = useSelector( - selectGtfsDatasetRoutesLoadingStatus, - ); const hasNoRoutes = totalRoutes == undefined || totalRoutes === 0; const getAndSetGeoJsonData = (urlToExtract: string): void => { @@ -174,7 +169,6 @@ const CoveredAreaMap: React.FC = ({ // for gtfs feeds if ( feed?.data_type === 'gtfs' && - routesJsonLoadingStatus != 'failed' && !hasNoRoutes && boundingBox != undefined ) { @@ -190,7 +184,7 @@ const CoveredAreaMap: React.FC = ({ return; } setView('boundingBoxView'); - }, [feed, routesJsonLoadingStatus, totalRoutes, boundingBox, geoJsonData]); + }, [feed, totalRoutes, boundingBox, geoJsonData]); const handleViewChange = ( _: React.MouseEvent, @@ -287,12 +281,9 @@ const CoveredAreaMap: React.FC = ({ const latestAutodiscoveryUrl = getGbfsLatestVersionVisualizationUrl(); const enableGtfsVisualizationView = useMemo(() => { return ( - feed?.data_type === 'gtfs' && - routesJsonLoadingStatus != 'failed' && - !hasNoRoutes && - boundingBox != undefined + feed?.data_type === 'gtfs' && !hasNoRoutes && boundingBox != undefined ); - }, [feed?.data_type, routesJsonLoadingStatus, hasNoRoutes, boundingBox]); + }, [feed?.data_type, hasNoRoutes, boundingBox]); return ( = ({ {(boundingBox != undefined || !geoJsonError) && ( - {geoJsonLoading || routesJsonLoadingStatus === 'loading' ? ( + {geoJsonLoading ? ( { - state.data = initialState.data; - state.loadedAllData = initialState.loadedAllData; - state.errors = initialState.errors; - state.status = initialState.status; - state.datasetId = initialState.datasetId; - }, - updateDatasetId: ( - state, - action: PayloadAction<{ - datasetId: string; - }>, - ) => { - state.datasetId = action.payload?.datasetId; - }, - loadingDataset: ( - state, - action: PayloadAction<{ - feedId: string; - offset?: number; - limit?: number; - }>, - ) => { - state.status = 'loading'; - state.errors = { - ...state.errors, - DatabaseAPI: initialState.errors.DatabaseAPI, - }; - }, - loadingDatasetSuccess: ( - state, - action: PayloadAction<{ - data: paths['/v1/gtfs_feeds/{id}/datasets']['get']['responses'][200]['content']['application/json']; - loadedAllData?: boolean; - }>, - ) => { - state.status = 'loaded'; - state.loadedAllData = action.payload?.loadedAllData; - state.data = mergeAndSortDatasets(action.payload?.data, state.data); - state.errors = { - ...state.errors, - DatabaseAPI: initialState.errors.DatabaseAPI, - }; - }, - loadingDatasetFail: (state, action: PayloadAction) => { - state.errors.DatabaseAPI = action.payload; - }, - }, -}); - -export const { - updateDatasetId, - loadingDataset, - loadingDatasetFail, - loadingDatasetSuccess, - clearDataset, -} = datasetSlice.actions; - -export default datasetSlice.reducer; diff --git a/src/app/store/dataset-selectors.ts b/src/app/store/dataset-selectors.ts deleted file mode 100644 index 0817020..0000000 --- a/src/app/store/dataset-selectors.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { type LngLatTuple } from '../types'; -import { type components, type paths } from '../services/feeds/types'; -import { type RootState } from './store'; -import { createSelector } from '@reduxjs/toolkit'; - -export const selectDatasetsData = ( - state: RootState, -): - | paths['/v1/gtfs_feeds/{id}/datasets']['get']['responses'][200]['content']['application/json'] - | undefined => { - return state.dataset.data; -}; - -export const selectDatasetsLoadingStatus = ( - state: RootState, -): 'loading' | 'loaded' => state.dataset.status; - -export const selectLatestDatasetsData = ( - state: RootState, -): components['schemas']['GtfsDataset'] | undefined => { - return state.dataset.data !== undefined ? state.dataset.data[0] : undefined; -}; - -export const selectHasLoadedAllDatasets = ( - state: RootState, -): boolean | undefined => { - return state.dataset.loadedAllData; -}; - -export const selectBoundingBoxFromLatestDataset = createSelector( - [selectLatestDatasetsData], - (latestDataset): LngLatTuple[] | undefined => { - if (latestDataset === undefined) return undefined; - return latestDataset.bounding_box?.minimum_latitude !== undefined && - latestDataset.bounding_box?.maximum_latitude !== undefined && - latestDataset.bounding_box?.minimum_longitude !== undefined && - latestDataset.bounding_box?.maximum_longitude !== undefined - ? [ - [ - latestDataset.bounding_box?.minimum_longitude, - latestDataset.bounding_box?.minimum_latitude, - ], - [ - latestDataset.bounding_box?.maximum_longitude, - latestDataset.bounding_box?.minimum_latitude, - ], - [ - latestDataset.bounding_box?.maximum_longitude, - latestDataset.bounding_box?.maximum_latitude, - ], - [ - latestDataset.bounding_box?.minimum_longitude, - latestDataset.bounding_box?.maximum_latitude, - ], - ] - : undefined; - }, -); diff --git a/src/app/store/feed-reducer.ts b/src/app/store/feed-reducer.ts deleted file mode 100644 index ef2487d..0000000 --- a/src/app/store/feed-reducer.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import { - type FeedErrors, - FeedErrorSource, - type FeedError, - type FeedStatus, -} from '../types'; -import { type GTFSRTFeedType, type AllFeedType } from '../services/feeds/utils'; - -interface FeedState { - status: FeedStatus; - feedId: string | undefined; - data: AllFeedType; - relatedFeedIds: string[]; - relatedFeedsData: { - gtfs: AllFeedType[]; - gtfsRt: GTFSRTFeedType[]; - }; - relatedFeedsStatus: 'loading' | 'loaded' | 'error'; - errors: FeedErrors; -} - -const initialState: FeedState = { - status: 'loaded', - feedId: undefined, - data: undefined, - relatedFeedIds: [], - relatedFeedsData: { - gtfs: [], - gtfsRt: [], - }, - relatedFeedsStatus: 'loading', - errors: { - [FeedErrorSource.DatabaseAPI]: null, - }, -}; - -export const feedSlice = createSlice({ - name: 'feedProfile', - initialState, - reducers: { - updateFeedId: ( - state, - action: PayloadAction<{ - feedId: string; - }>, - ) => { - state.feedId = action.payload?.feedId; - }, - resetFeed: (state) => { - state = { - ...initialState, - }; - }, - loadingFeed: ( - state, - action: PayloadAction<{ - feedId: string; - feedDataType?: string; - }>, - ) => { - state.status = 'loading'; - state.data = undefined; - state.errors = { - ...state.errors, - DatabaseAPI: initialState.errors.DatabaseAPI, - }; - }, - loadingFeedSuccess: ( - state, - action: PayloadAction<{ - data: AllFeedType; - }>, - ) => { - state.status = 'loaded'; - state.data = action.payload?.data; - state.feedId = action.payload.data?.id; - state.errors = { - ...state.errors, - DatabaseAPI: initialState.errors.DatabaseAPI, - }; - }, - loadingFeedFail: (state, action: PayloadAction) => { - state.status = 'error'; - state.errors.DatabaseAPI = action.payload; - }, - loadingRelatedFeeds: ( - state, - action: PayloadAction<{ - feedIds: string[]; - }>, - ) => { - state.relatedFeedsStatus = 'loading'; - state.relatedFeedsData = { - gtfs: [], - gtfsRt: [], - }; - state.errors = { - ...state.errors, - DatabaseAPI: initialState.errors.DatabaseAPI, - }; - }, - loadingRelatedFeedsSuccess: ( - state, - action: PayloadAction<{ - data: { - gtfs: AllFeedType[]; - gtfsRt: GTFSRTFeedType[]; - }; - }>, - ) => { - state.relatedFeedsStatus = 'loaded'; - state.relatedFeedsData = action.payload.data; - state.errors = { - ...state.errors, - DatabaseAPI: initialState.errors.DatabaseAPI, - }; - }, - loadingRelatedFeedsFail: (state, action: PayloadAction) => { - state.relatedFeedsStatus = 'error'; - state.errors.DatabaseAPI = action.payload; - }, - }, -}); - -export const { - updateFeedId, - resetFeed, - loadingFeed, - loadingFeedFail, - loadingFeedSuccess, - loadingRelatedFeeds, - loadingRelatedFeedsFail, - loadingRelatedFeedsSuccess, -} = feedSlice.actions; - -export default feedSlice.reducer; diff --git a/src/app/store/feed-selectors.ts b/src/app/store/feed-selectors.ts deleted file mode 100644 index e5552cb..0000000 --- a/src/app/store/feed-selectors.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { - type AllFeedType, - type BasicFeedType, - type GBFSFeedType, - type GBFSVersionType, - type GTFSFeedType, - type GTFSRTFeedType, - isGbfsFeedType, - isGtfsFeedType, - isGtfsRtFeedType, -} from '../services/feeds/utils'; -import { type RootState } from './store'; -import { type LngLatTuple } from '../types'; - -export const selectFeedData = (state: RootState): BasicFeedType => { - return state.feedProfile.data; -}; - -export const selectFeedLoadingStatus = (state: RootState): string => { - return state.feedProfile.status; -}; -export const selectGTFSFeedData = (state: RootState): GTFSFeedType => { - return isGtfsFeedType(state.feedProfile.data) - ? state.feedProfile.data - : undefined; -}; -export const selectGTFSRTFeedData = (state: RootState): GTFSRTFeedType => { - return isGtfsRtFeedType(state.feedProfile.data) - ? state.feedProfile.data - : undefined; -}; -export const selectGBFSFeedData = (state: RootState): GBFSFeedType => { - return isGbfsFeedType(state.feedProfile.data) - ? state.feedProfile.data - : undefined; -}; - -export const selectFeedId = (state: RootState): string => { - return state.feedProfile.feedId ?? 'mdb-1'; -}; - -export const selectLatestGbfsVersion = ( - state: RootState, -): GBFSVersionType | undefined => { - if (state.feedProfile.data?.data_type === 'gbfs') { - const autodiscoveryVersion = ( - state.feedProfile.data as GBFSFeedType - )?.versions?.find((v) => v.source === 'autodiscovery'); - if (autodiscoveryVersion !== undefined) { - return autodiscoveryVersion; - } - const gbfsFeed: GBFSFeedType = state.feedProfile.data; - const sortedVersions = gbfsFeed?.versions - ?.filter((v) => v.version !== undefined) - .sort((a, b) => { - if (a.version === undefined) return -1; - if (b.version === undefined) return 1; - if (a.version < b.version) return 1; - if (a.version > b.version) return -1; - return 0; - }); - if (sortedVersions !== undefined && sortedVersions.length > 0) { - return sortedVersions[0]; - } - } - return undefined; -}; - -export const selectLatestGtfsDatasetId = ( - state: RootState, -): string | undefined => { - if (state.feedProfile.data?.data_type === 'gtfs') { - const gtfsFeed: GTFSFeedType = state.feedProfile.data; - return gtfsFeed?.latest_dataset?.id ?? undefined; - } -}; - -export const selectAutoDiscoveryUrl = ( - state: RootState, -): string | undefined => { - if (state.feedProfile.data?.data_type === 'gbfs') { - const gbfsFeed: GBFSFeedType = state.feedProfile.data; - return ( - gbfsFeed?.versions - ?.find((v) => v.source === 'autodiscovery') - ?.endpoints?.find((e) => e.name === 'gbfs')?.url ?? - gbfsFeed?.source_info?.producer_url - ); - } -}; - -export const selectRelatedFeedsData = (state: RootState): AllFeedType[] => { - return state.feedProfile.relatedFeedsData.gtfs; -}; -export const selectRelatedGtfsRTFeedsData = ( - state: RootState, -): GTFSRTFeedType[] => { - return state.feedProfile.relatedFeedsData.gtfsRt; -}; - -export const selectFeedBoundingBox = ( - state: RootState, -): LngLatTuple[] | undefined => { - if ( - !( - isGtfsFeedType(state.feedProfile.data) || - isGbfsFeedType(state.feedProfile.data) - ) - ) { - return undefined; - } - const feed = state.feedProfile.data; - - if ( - feed?.bounding_box?.maximum_latitude == undefined || - feed?.bounding_box.maximum_longitude == undefined || - feed?.bounding_box.minimum_latitude == undefined || - feed?.bounding_box.minimum_longitude == undefined - ) { - return undefined; - } - return [ - [feed.bounding_box.minimum_longitude, feed.bounding_box.minimum_latitude], - [feed.bounding_box.maximum_longitude, feed.bounding_box.minimum_latitude], - [feed.bounding_box.maximum_longitude, feed.bounding_box.maximum_latitude], - [feed.bounding_box.minimum_longitude, feed.bounding_box.maximum_latitude], - ]; -}; diff --git a/src/app/store/feeds-reducer.ts b/src/app/store/feeds-reducer.ts deleted file mode 100644 index 5731d8c..0000000 --- a/src/app/store/feeds-reducer.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import { type FeedErrors, FeedErrorSource, type FeedError } from '../types'; -import { - type AllFeedsParams, - type AllFeedsType, -} from '../services/feeds/utils'; - -interface FeedsState { - status: 'loading' | 'loaded' | 'error'; - data: AllFeedsType | undefined; - errors: FeedErrors; -} - -const initialState: FeedsState = { - status: 'loading', - data: undefined, - errors: { - [FeedErrorSource.DatabaseAPI]: null, - }, -}; - -export const feedsSlice = createSlice({ - name: 'feeds', - initialState, - reducers: { - resetFeeds: (state) => { - state.status = 'loaded'; - state.data = { - total: 0, - results: [], - }; - }, - loadingFeeds: ( - state, - action: PayloadAction<{ - params: AllFeedsParams; - }>, - ) => { - state.status = 'loading'; - state.data = undefined; - state.errors = { - ...state.errors, - DatabaseAPI: initialState.errors.DatabaseAPI, - }; - }, - loadingFeedsSuccess: ( - state, - action: PayloadAction<{ - data: AllFeedsType; - }>, - ) => { - state.status = 'loaded'; - state.data = action.payload.data; - state.errors = { - ...state.errors, - DatabaseAPI: initialState.errors.DatabaseAPI, - }; - }, - loadingFeedsFail: (state, action: PayloadAction) => { - state.status = 'error'; - state.errors.DatabaseAPI = action.payload; - }, - }, -}); - -export const { - resetFeeds, - loadingFeeds, - loadingFeedsFail, - loadingFeedsSuccess, -} = feedsSlice.actions; - -export default feedsSlice.reducer; diff --git a/src/app/store/feeds-selectors.ts b/src/app/store/feeds-selectors.ts deleted file mode 100644 index c04807d..0000000 --- a/src/app/store/feeds-selectors.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type AllFeedsType } from '../services/feeds/utils'; -import { type FeedStatus } from '../types'; -import { type RootState } from './store'; - -export const selectFeedsData = (state: RootState): AllFeedsType | undefined => { - return state.feeds.data; -}; - -export const selectFeedsStatus = (state: RootState): FeedStatus => { - return state.feeds.status; -}; diff --git a/src/app/store/gbfs-analytics-selector.ts b/src/app/store/gbfs-analytics-selector.ts index 827c7a9..c650b74 100644 --- a/src/app/store/gbfs-analytics-selector.ts +++ b/src/app/store/gbfs-analytics-selector.ts @@ -1,4 +1,3 @@ -import { createSelector } from '@reduxjs/toolkit'; import { type RootState } from './store'; import { type GBFSFeedMetrics } from '../utils/analytics-types'; @@ -15,15 +14,3 @@ export const selectGBFSAnalyticsStatus = ( export const selectGBFSAnalyticsError = ( state: RootState, ): string | undefined => state.gbfsAnalytics.error; - -// Selector to get the list of available analytics files -export const selectAvailableGBFSFiles = createSelector( - (state: RootState) => state.gbfsAnalytics.availableFiles, - (availableFiles) => availableFiles, -); - -// Selector to get the currently selected file -export const selectSelectedGBFSFile = createSelector( - (state: RootState) => state.gbfsAnalytics.selectedFile, - (selectedFile) => selectedFile, -); diff --git a/src/app/store/profile-reducer.ts b/src/app/store/profile-reducer.ts index cccfac4..bbb6045 100644 --- a/src/app/store/profile-reducer.ts +++ b/src/app/store/profile-reducer.ts @@ -124,9 +124,6 @@ export const userProfileSlice = createSlice({ state.errors = { ...initialState.errors, SignUp: action.payload }; state.isAppRefreshing = false; }, - resetProfileErrors: (state) => { - state.errors = { ...initialState.errors }; - }, requestRefreshAccessToken: (state) => { state.isRefreshingAccessToken = true; state.errors = { ...initialState.errors }; @@ -278,7 +275,6 @@ export const { signUp, signUpSuccess, signUpFail, - resetProfileErrors, refreshAccessToken, refreshAccessTokenFail, requestRefreshAccessToken, diff --git a/src/app/store/profile-selectors.ts b/src/app/store/profile-selectors.ts index 84597f7..a86cc45 100644 --- a/src/app/store/profile-selectors.ts +++ b/src/app/store/profile-selectors.ts @@ -22,7 +22,7 @@ export const selectIsTokenRefreshed = (state: RootState): boolean => export const selectIsVerificationEmailSent = (state: RootState): boolean => state.userProfile.isVerificationEmailSent; -export const selectErrorBySource = ( +const selectErrorBySource = ( state: RootState, source: ProfileErrorSource, ): ProfileError | null => state.userProfile.errors[source]; @@ -69,6 +69,3 @@ export const selectRegistrationError = ( state: RootState, ): ProfileError | null => selectErrorBySource(state, ProfileErrorSource.Registration); - -export const selectUserEmail = (state: RootState): string | undefined => - state.userProfile.user?.email; diff --git a/src/app/store/reducers.ts b/src/app/store/reducers.ts index 01e5733..3562635 100644 --- a/src/app/store/reducers.ts +++ b/src/app/store/reducers.ts @@ -1,22 +1,14 @@ import { combineReducers } from 'redux'; import profileReducer from './profile-reducer'; -import feedReducer from './feed-reducer'; -import datasetReducer from './dataset-reducer'; -import feedsReducer from './feeds-reducer'; import GTFSAnalyticsReducer from './gtfs-analytics-reducer'; import GBFSAnalyticsReducer from './gbfs-analytics-reducer'; -import supportingFilesReducer from './supporting-files-reducer'; import gbfsValidatorReducer from './gbfs-validator-reducer'; import licenseReducer from './license-reducer'; const rootReducer = combineReducers({ userProfile: profileReducer, - feedProfile: feedReducer, - dataset: datasetReducer, - feeds: feedsReducer, gtfsAnalytics: GTFSAnalyticsReducer, gbfsAnalytics: GBFSAnalyticsReducer, - supportingFiles: supportingFilesReducer, gbfsValidator: gbfsValidatorReducer, licenseProfile: licenseReducer, }); diff --git a/src/app/store/saga/dataset-saga.ts b/src/app/store/saga/dataset-saga.ts deleted file mode 100644 index 360f8ba..0000000 --- a/src/app/store/saga/dataset-saga.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { call, takeLatest, put } from 'redux-saga/effects'; -import { loadingFeedFail } from '../feed-reducer'; -import { getAppError } from '../../utils/error'; -import { DATASET_LOADING_FEED, type FeedError } from '../../types'; -import { type PayloadAction } from '@reduxjs/toolkit'; -import { getGtfsFeedDatasets } from '../../services/feeds'; -import { type paths } from '../../services/feeds/types'; -import { loadingDatasetSuccess } from '../dataset-reducer'; -import { getUserAccessToken } from '../../services'; -import { areAllDatasetsLoaded } from '../../utils/dataset'; - -function* getDatasetSaga({ - payload: { feedId, offset, limit }, -}: PayloadAction<{ - feedId: string; - offset?: number; - limit?: number; -}>): Generator { - try { - if (feedId !== undefined) { - const accessToken = (yield call(getUserAccessToken)) as string; - const datasets = (yield call(getGtfsFeedDatasets, feedId, accessToken, { - offset, - limit, - })) as paths['/v1/gtfs_feeds/{id}/datasets']['get']['responses'][200]['content']['application/json']; - const hasLoadedAllData = areAllDatasetsLoaded( - datasets.length, - limit, - offset, - ); - yield put( - loadingDatasetSuccess({ - data: datasets, - loadedAllData: hasLoadedAllData, - }), - ); - } - } catch (error) { - yield put(loadingFeedFail(getAppError(error) as FeedError)); - } -} - -export function* watchDataset(): Generator { - yield takeLatest(DATASET_LOADING_FEED, getDatasetSaga); -} diff --git a/src/app/store/saga/feed-saga.ts b/src/app/store/saga/feed-saga.ts deleted file mode 100644 index b404dbb..0000000 --- a/src/app/store/saga/feed-saga.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { - type StrictEffect, - all, - call, - takeLatest, - put, -} from 'redux-saga/effects'; -import { - loadingFeedFail, - loadingFeedSuccess, - loadingRelatedFeedsFail, - loadingRelatedFeedsSuccess, -} from '../feed-reducer'; -import { getAppError } from '../../utils/error'; -import { - FEED_PROFILE_LOADING_FEED, - FEED_PROFILE_LOADING_RELATED_FEEDS, - type FeedError, -} from '../../types'; -import { type PayloadAction } from '@reduxjs/toolkit'; -import { - getFeed, - getGbfsFeed, - getGtfsFeed, - getGtfsFeedAssociatedGtfsRtFeeds, - getGtfsRtFeed, -} from '../../services/feeds'; -import { - type GTFSRTFeedType, - type AllFeedType, -} from '../../services/feeds/utils'; -import { getUserAccessToken } from '../../services'; - -function* getFeedSaga({ - payload: { feedId, feedDataType }, -}: PayloadAction<{ feedId: string; feedDataType?: string }>): Generator< - StrictEffect, - void, - AllFeedType -> { - try { - if (feedId !== undefined) { - const accessToken = (yield call(getUserAccessToken)) as string; - - if (feedDataType == undefined) { - const basicFeed = yield call(getFeed, feedId, accessToken); - feedDataType = basicFeed?.data_type; - } - - let feed: AllFeedType; - if (feedDataType === 'gtfs') { - feed = yield call(getGtfsFeed, feedId, accessToken); - } else if (feedDataType === 'gtfs_rt') { - feed = yield call(getGtfsRtFeed, feedId, accessToken); - } else if (feedDataType === 'gbfs') { - feed = yield call(getGbfsFeed, feedId, accessToken); - } else { - throw new Error('Invalid feed data type'); - } - - yield put(loadingFeedSuccess({ data: feed })); - } - } catch (error) { - yield put(loadingFeedFail(getAppError(error) as FeedError)); - } -} - -function* getRelatedFeedsSaga({ - payload: { feedIds }, -}: PayloadAction<{ feedIds: string[] }>): Generator { - try { - if (feedIds.length > 0) { - const accessToken = (yield call(getUserAccessToken)) as string; - const feedsData: AllFeedType[] = (yield all( - feedIds.map((feedId) => - call( - function* ( - feedId: string, - accessToken: string, - ): Generator { - const feed = yield call(getGtfsFeed, feedId, accessToken); - return feed; - }, - feedId, - accessToken, - ), - ), - )) as AllFeedType[]; - - const gtfsRtFeedsData = (yield all( - feedIds.map((feedId) => - call(getGtfsFeedAssociatedGtfsRtFeeds, feedId, accessToken), - ), - )) as GTFSRTFeedType[]; - const flattenedGtfsRtFeedsData = gtfsRtFeedsData.flat(); - const uniqueGtfsRtFeedsData: GTFSRTFeedType[] = []; - const uniqueFeedRtIds = new Set(); - flattenedGtfsRtFeedsData.forEach((feed) => { - if (uniqueFeedRtIds.has(feed?.id)) return; - uniqueGtfsRtFeedsData.push(feed); - uniqueFeedRtIds.add(feed?.id); - }); - yield put( - loadingRelatedFeedsSuccess({ - data: { - gtfs: feedsData, - gtfsRt: uniqueGtfsRtFeedsData, - }, - }), - ); - } - } catch (error) { - yield put(loadingRelatedFeedsFail(getAppError(error) as FeedError)); - } -} - -export function* watchFeed(): Generator { - yield takeLatest(FEED_PROFILE_LOADING_FEED, getFeedSaga); - yield takeLatest(FEED_PROFILE_LOADING_RELATED_FEEDS, getRelatedFeedsSaga); -} diff --git a/src/app/store/saga/feeds-saga.ts b/src/app/store/saga/feeds-saga.ts deleted file mode 100644 index 9c03040..0000000 --- a/src/app/store/saga/feeds-saga.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { takeLatest, put, call } from 'redux-saga/effects'; -import { getAppError } from '../../utils/error'; -import { FEEDS_LOADING_FEEDS, type FeedError } from '../../types'; -import { type PayloadAction } from '@reduxjs/toolkit'; -import { - type AllFeedsType, - type AllFeedsParams, -} from '../../services/feeds/utils'; -import { loadingFeedsFail, loadingFeedsSuccess } from '../feeds-reducer'; -import { searchFeeds } from '../../services/feeds'; -import { getUserAccessToken } from '../../services/profile-service'; - -function* getFeedsSaga({ - payload: { params }, -}: PayloadAction<{ - params: AllFeedsParams; - accessToken: string; -}>): Generator { - try { - const accessToken = (yield call(getUserAccessToken)) as string; - const searchData: AllFeedsType = (yield call( - searchFeeds, - params, - accessToken, - )) as AllFeedsType; - yield put(loadingFeedsSuccess({ data: searchData })); - } catch (error) { - yield put(loadingFeedsFail(getAppError(error) as FeedError)); - } -} - -export function* watchFeeds(): Generator { - yield takeLatest(FEEDS_LOADING_FEEDS, getFeedsSaga); -} diff --git a/src/app/store/saga/root-saga.ts b/src/app/store/saga/root-saga.ts index a46ba83..24319fb 100644 --- a/src/app/store/saga/root-saga.ts +++ b/src/app/store/saga/root-saga.ts @@ -1,12 +1,8 @@ import { all } from 'redux-saga/effects'; import { watchAuth } from './auth-saga'; import { watchProfile } from './profile-saga'; -import { watchFeed } from './feed-saga'; -import { watchDataset } from './dataset-saga'; -import { watchFeeds } from './feeds-saga'; import { watchGTFSFetchFeedMetrics } from './gtfs-analytics-saga'; import { watchGBFSFetchFeedMetrics } from './gbfs-analytics-saga'; -import { watchSupportingFiles } from './supporting-files-saga'; import { watchGbfsValidator } from './gbfs-validator-saga'; import { watchLicense } from './license-saga'; @@ -14,12 +10,8 @@ const rootSaga = function* (): Generator { yield all([ watchAuth(), watchProfile(), - watchFeed(), - watchDataset(), - watchFeeds(), watchGTFSFetchFeedMetrics(), watchGBFSFetchFeedMetrics(), - watchSupportingFiles(), watchGbfsValidator(), watchLicense(), ]); diff --git a/src/app/store/saga/supporting-files-saga.spec.ts b/src/app/store/saga/supporting-files-saga.spec.ts deleted file mode 100644 index 92246e0..0000000 --- a/src/app/store/saga/supporting-files-saga.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { runSaga } from 'redux-saga'; -import * as http from '../../services/http'; -import { - loadingSupportingFile, - loadingSupportingFileSuccess, - loadingSupportingFileFail, -} from '../supporting-files-reducer'; -import { - loadSupportingFileSaga, - buildRoutesUrl, -} from './supporting-files-saga'; - -// Mock the entire http module -jest.mock('../../services/http', () => ({ - getJson: jest.fn(), -})); - -const mockedHttp = http as jest.Mocked; - -describe('supporting-files-saga', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('worker saga dispatches success when getJson resolves', async () => { - const fakeData = { - type: 'FeatureCollection', - features: [], - extracted_at: '2025-01-01T00:00:00Z', - extraction_url: 'http://example.com/source', - }; - mockedHttp.getJson.mockResolvedValue(fakeData); - - const dispatched: unknown[] = []; - - await runSaga( - { - dispatch: (action) => dispatched.push(action), - getState: () => ({}), - }, - loadSupportingFileSaga, - loadingSupportingFile({ - key: 'gtfsGeolocationGeojson', - url: 'http://example.com', - }), - ).toPromise(); - - expect(mockedHttp.getJson).toHaveBeenCalledWith('http://example.com'); - expect(dispatched).toContainEqual( - loadingSupportingFileSuccess({ - key: 'gtfsGeolocationGeojson', - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - data: fakeData, - }), - ); - }); - - it('worker saga dispatches fail when getJson throws', async () => { - mockedHttp.getJson.mockRejectedValue(new Error('Network error')); - - const dispatched: unknown[] = []; - - await runSaga( - { - dispatch: (action) => dispatched.push(action), - getState: () => ({}), - }, - loadSupportingFileSaga, - loadingSupportingFile({ - key: 'gtfsGeolocationGeojson', - url: 'http://example.com', - }), - ).toPromise(); - - expect(mockedHttp.getJson).toHaveBeenCalledWith('http://example.com'); - expect( - dispatched.some( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - (action) => action.type === loadingSupportingFileFail.type, - ), - ).toBe(true); - }); - - it('buildRoutesUrl creates correct URL', () => { - const url = buildRoutesUrl('mdb-1', 'mdb-1-20250101'); - expect(url).toContain( - 'files.mobilitydatabase.org/mdb-1/mdb-1-20250101/pmtiles/routes.json', - ); - }); -}); diff --git a/src/app/store/saga/supporting-files-saga.ts b/src/app/store/saga/supporting-files-saga.ts deleted file mode 100644 index 4de8dac..0000000 --- a/src/app/store/saga/supporting-files-saga.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { call, put, takeLatest, select } from 'redux-saga/effects'; -import { - loadingSupportingFile, - loadingSupportingFileSuccess, - loadingSupportingFileFail, - setSupportingFilesContext, - clearSupportingFiles, -} from '../supporting-files-reducer'; -import { getAppError } from '../../utils/error'; -import { getJson } from '../../services/http'; -import { - updateFeedId, - loadingFeedSuccess, - loadingFeedFail, -} from '../feed-reducer'; -import { selectFeedData } from '../feed-selectors'; -import { - type GtfsRoute, - type GeoJSONData, - type GeoJSONDataGBFS, -} from '../../types'; -import { getFeedFilesBaseUrl } from '../../utils/config'; -import { type GTFSFeedType } from '../../services/feeds/utils'; - -export function buildRoutesUrl(feedId: string, datasetId: string): string { - return `${getFeedFilesBaseUrl()}/${feedId}/${datasetId}/pmtiles/routes.json`; -} - -export function* loadSupportingFileSaga({ - payload: { key, url }, -}: ReturnType): Generator< - unknown, - void, - unknown -> { - try { - const data = (yield call(getJson, url)) as - | GeoJSONData - | GeoJSONDataGBFS - | GtfsRoute[]; - // Dispatch success with the parsed data - yield put(loadingSupportingFileSuccess({ key, data })); - } catch (error) { - const appError = getAppError(error); - const message = appError.message; - yield put(loadingSupportingFileFail({ key, error: message })); - } -} - -const handleFeedChangeFail = function* (): Generator { - // Clear any supporting files loaded for a previous feed. - yield put(clearSupportingFiles()); -}; - -const handleFeedChange = function* (): Generator { - const feed = (yield select(selectFeedData)) as - | { id?: string; data_type?: string } - | undefined; - const feedId = feed?.id; - const dataType = feed?.data_type; - - // Read previous feedId from supporting-files context so we only clear/reload - // when the feed actually changed. - const previousContext = (yield select((s) => s.supportingFiles)) as - | { context?: { feedId?: string } } - | undefined; - const previousFeedId = previousContext?.context?.feedId; - - // If feedId hasn't changed or it's, do nothing. - if (previousFeedId === feedId) { - return; - } - // This function it's only applied to gbfs feeds. GTFS feeds are processed when dataset is loaded - - // Set the context in the supporting-files state so we can track which - // feed the files belong to. - yield put(setSupportingFilesContext({ feedId, dataType })); - - // Clear any supporting files loaded for a previous feed. - yield put(clearSupportingFiles()); - - if (feed?.data_type === 'gtfs') { - if (feedId !== undefined) { - const gtfsFeed: GTFSFeedType = feed as GTFSFeedType; - const url = buildRoutesUrl( - feedId, - gtfsFeed?.visualization_dataset_id ?? '', - ); - yield put(loadingSupportingFile({ key: 'gtfsDatasetRoutesJson', url })); - } - } -}; - -export function* watchSupportingFiles(): Generator { - yield takeLatest(updateFeedId.type, handleFeedChange); - yield takeLatest(loadingFeedSuccess.type, handleFeedChange); - yield takeLatest(loadingFeedFail.type, handleFeedChangeFail); - yield takeLatest(loadingSupportingFile.type, loadSupportingFileSaga); -} diff --git a/src/app/store/selectors.ts b/src/app/store/selectors.ts index 8c2b6f6..66eaf74 100644 --- a/src/app/store/selectors.ts +++ b/src/app/store/selectors.ts @@ -1,16 +1 @@ -import { type RootState } from './store'; - export * from './profile-selectors'; -export * from './feed-selectors'; -export * from './dataset-selectors'; -export * from './supporting-files-selectors'; - -export const selectLoadingApp = (state: RootState): boolean => { - return ( - state.userProfile.status === 'login_in' || - state.userProfile.status === 'registering' || - state.userProfile.status === 'login_out' || - state.userProfile.status === 'sign_up' || - state.userProfile.isAppRefreshing - ); -}; diff --git a/src/app/store/supporting-files-reducer.ts b/src/app/store/supporting-files-reducer.ts deleted file mode 100644 index 87ec1c0..0000000 --- a/src/app/store/supporting-files-reducer.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; -import { - type SupportingFileKey, - type SupportingFile, - type GeoJSONData, - type GeoJSONDataGBFS, - type GtfsRoute, -} from '../types'; - -type SupportingFilesState = { - [K in SupportingFileKey]: SupportingFile; -}; - -// Top-level context to track which feed these supporting files belong to -interface SupportingFilesContext { - feedId?: string; - datasetId?: string; - dataType?: string; -} - -type FullSupportingFilesState = { - context: SupportingFilesContext; -} & SupportingFilesState; - -const initialState: FullSupportingFilesState = { - context: { - feedId: undefined, - datasetId: undefined, - dataType: undefined, - }, - gtfsGeolocationGeojson: { - key: 'gtfsGeolocationGeojson', - status: 'uninitialized', - }, - gtfsDatasetRoutesJson: { - key: 'gtfsDatasetRoutesJson', - status: 'uninitialized', - }, -}; - -export const supportingFilesSlice = createSlice({ - name: 'dataset', - initialState, - reducers: { - clearSupportingFiles: (state) => { - // Reset every supporting-file key to its initial value. Preserve the - // context unless caller explicitly resets it via `setSupportingFilesContext`. - const keys: Array = [ - 'gtfsGeolocationGeojson', - 'gtfsDatasetRoutesJson', - ]; - keys.forEach((key) => { - state[key] = initialState[key]; - }); - }, - // Set the current feed context for supporting files. When feedId changes - // the saga will clear and reload supporting files for the new feed. - setSupportingFilesContext: ( - state, - action: PayloadAction<{ feedId?: string; dataType?: string }>, - ) => { - const { feedId, dataType } = action.payload ?? {}; - state.context.feedId = feedId; - state.context.dataType = dataType; - }, - loadingSupportingFile: ( - state, - action: PayloadAction<{ - key: SupportingFileKey; - url: string; - }>, - ) => { - const { key } = action.payload; - state[key].status = 'loading'; - state[key].data = initialState[key].data; - state[key].error = initialState[key].error; - }, - loadingSupportingFileSuccess: ( - state, - action: PayloadAction<{ - data: GeoJSONData | GeoJSONDataGBFS | GtfsRoute[]; - key: SupportingFileKey; - }>, - ) => { - const { key, data } = action.payload; - state[key].status = 'loaded'; - state[key].data = data; - }, - loadingSupportingFileFail: ( - state, - action: PayloadAction<{ key: SupportingFileKey; error: string }>, - ) => { - const { key, error } = action.payload; - state[key].status = 'failed'; - state[key].error = error; - }, - }, -}); - -export const { - clearSupportingFiles, - setSupportingFilesContext, - loadingSupportingFile, - loadingSupportingFileSuccess, - loadingSupportingFileFail, -} = supportingFilesSlice.actions; - -export default supportingFilesSlice.reducer; diff --git a/src/app/store/supporting-files-selectors.ts b/src/app/store/supporting-files-selectors.ts deleted file mode 100644 index 8929335..0000000 --- a/src/app/store/supporting-files-selectors.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { type GtfsRoute, type GeoJSONData } from '../types'; -import { type RootState } from './store'; - -/** - * Selector to retrieve the GTFS geolocation GeoJSON data from the Redux store. - * - * @param state - The root Redux state. - * @returns The GeoJSONData object if available and valid, otherwise undefined. - */ -export const selectGtfsGeolocationGeojson = ( - state: RootState, -): GeoJSONData | undefined => { - const gtfsGeolocationGeojson = state.supportingFiles.gtfsGeolocationGeojson; - const data = state.supportingFiles.gtfsGeolocationGeojson.data; - if (gtfsGeolocationGeojson.key == 'gtfsGeolocationGeojson') { - return data as GeoJSONData; - } - return undefined; -}; - -/** - * Selector to the loading status of the GTFS dataset routes JSON from the Redux store. - * @param state - The root Redux state. - * @returns The loading status as a string (e.g., 'loading', 'loaded', 'error'). - */ -export const selectGtfsDatasetRoutesLoadingStatus = ( - state: RootState, -): string => { - return state.supportingFiles.gtfsDatasetRoutesJson.status; -}; - -/** - * Selector to retrieve the GTFS dataset routes JSON from the Redux store. - * - * @param state - The root Redux state. - * @returns An array of GtfsRoute objects if available and valid, otherwise undefined. - */ -export const selectGtfsDatasetRoutesJson = ( - state: RootState, -): GtfsRoute[] | undefined => { - const gtfsDatasetRoutesJson = state.supportingFiles.gtfsDatasetRoutesJson; - const data = state.supportingFiles.gtfsDatasetRoutesJson.data; - if ( - gtfsDatasetRoutesJson != undefined && - gtfsDatasetRoutesJson.key == 'gtfsDatasetRoutesJson' - ) { - return data as GtfsRoute[]; - } - return undefined; -}; - -/** - * Selector to compute the total number of rows in the GTFS dataset routes JSON. - * - * @param selectGtfsDatasetRoutesJson - An array of GtfsRoute objects if available and valid, or undefined. - * @returns The total number of rows as a number, or undefined if not available. - */ -export const selectGtfsDatasetRoutesTotal = createSelector( - [selectGtfsDatasetRoutesJson], - (gtfsDatasetRoutesJson: GtfsRoute[] | undefined): number | undefined => { - if (gtfsDatasetRoutesJson == undefined) { - return undefined; - } - return gtfsDatasetRoutesJson.length; - }, -); - -function isValidNumber(str: string): boolean { - if (typeof str !== 'string') return false; - const trimmed = str.trim(); - return trimmed !== '' && Number.isFinite(Number(trimmed)); -} - -/** - * Selector that derives the unique list of GTFS route types from the stored GTFS dataset routes JSON. - * - * This selector: - * - Reads the GTFS routes array from the Redux store (via selectGtfsDatasetRoutesJson). - * - Normalizes each routeType by converting to a trimmed string and ignoring null/undefined/empty values. - * - Deduplicates routeType values. - * - Sorts the resulting unique values with numeric-aware ordering: - * - If both values are numeric strings, they are compared by numeric value. - * - Numeric strings are ordered before non-numeric strings. - * - Non-numeric strings are compared lexicographically (localeCompare). - * - * @param state - The root Redux state. - * @returns An array of unique, sorted route-type strings (e.g. ["0", "1", "bus", "tram"]), or `undefined` if the GTFS dataset routes JSON is not available. - */ -export const selectGtfsDatasetRouteTypes = createSelector( - [selectGtfsDatasetRoutesJson], - (gtfsDatasetRoutesJson: GtfsRoute[] | undefined): string[] | undefined => { - if (gtfsDatasetRoutesJson == undefined) { - return undefined; - } - const uniqueRouteTypesSet = new Set(); - for (const route of gtfsDatasetRoutesJson) { - const raw = route.routeType; - const routeTypeStr = raw == null ? undefined : String(raw).trim(); - if (routeTypeStr != undefined) { - uniqueRouteTypesSet.add(routeTypeStr); - } - } - const uniqueRouteTypes = Array.from(uniqueRouteTypesSet); - return uniqueRouteTypes.sort((a, b) => { - const validNumberA = isValidNumber(a); - const validNumberB = isValidNumber(b); - // if both are not numbers, sort as string - if (!validNumberA && !validNumberB) { - return a.localeCompare(b); - } - // If one is not a number, number should be first - if (!validNumberA || !validNumberB) { - return validNumberA ? -1 : 1; - } - // if both are numbers then, apply number sorting - return Number(a) - Number(b); - }); - }, -); diff --git a/src/app/types.ts b/src/app/types.ts index 87a2886..2c1196b 100644 --- a/src/app/types.ts +++ b/src/app/types.ts @@ -52,32 +52,11 @@ export const USER_PROFILE_REFRESH_INFORMATION = `${USER_PROFILE}/refreshUserInfo export const USER_PROFILE_RESET_PASSWORD = `${USER_PROFILE}/resetPassword`; export const USER_PROFILE_ANONYMOUS_LOGIN = `${USER_PROFILE}/anonymousLogin`; -export const FEED_PROFILE = 'feedProfile'; - -export const FEED_PROFILE_UPDATE_FEED_ID = `${FEED_PROFILE}/updateFeedId`; -export const FEED_PROFILE_RESET_FEED = `${FEED_PROFILE}/resetFeed`; -export const FEED_PROFILE_LOADING_FEED = `${FEED_PROFILE}/loadingFeed`; -export const FEED_PROFILE_LOADING_FEED_SUCCESS = `${FEED_PROFILE}/loadingFeedSuccess`; -export const FEED_PROFILE_LOADING_FEED_FAIL = `${FEED_PROFILE}/loadingFeedFail`; -export const FEED_PROFILE_LOADING_RELATED_FEEDS = `${FEED_PROFILE}/loadingRelatedFeeds`; -export const FEED_PROFILE_LOADING_RELATED_FEEDS_SUCCESS = `${FEED_PROFILE}/loadingRelatedFeedsSuccess`; -export const FEED_PROFILE_LOADING_RELATED_FEEDS_FAIL = `${FEED_PROFILE}/loadingRelatedFeedsFail`; - export const LICENSE_PROFILE = 'licenseProfile'; export const LICENSE_PROFILE_LOADING_LICENSE = `${LICENSE_PROFILE}/loadingLicense`; export const LICENSE_PROFILE_LOADING_LICENSE_SUCCESS = `${LICENSE_PROFILE}/loadingLicenseSuccess`; export const LICENSE_PROFILE_LOADING_LICENSE_FAIL = `${LICENSE_PROFILE}/loadingLicenseFail`; -export const FEEDS_RESET_FEEDS = `feeds/resetFeeds`; -export const FEEDS_LOADING_FEEDS = `feeds/loadingFeeds`; -export const FEEDS_LOADING_FEEDS_SUCCESS = `feeds/loadingFeedsSuccess`; -export const FEEDS_LOADING_FEEDS_FAIL = `feeds/loadingFeedsFail`; - -export const DATASET_UPDATE_FEED_ID = `dataset/updateDatasetId`; -export const DATASET_LOADING_FEED = `dataset/loadingDataset`; -export const DATASET_LOADING_FEED_SUCCESS = `dataset/loadingDatasetSuccess`; -export const DATASET_LOADING_FEED_FAIL = `dataset/loadingDatasetFail`; - export enum ProfileErrorSource { SignUp = 'SignUp', Login = 'Login', @@ -175,21 +154,3 @@ export interface GtfsRoute { * This replaces the Leaflet LatLngTuple ([lat, lng]) that was previously used. */ export type LngLatTuple = [number, number]; - -export type LoadStatus = 'uninitialized' | 'loading' | 'loaded' | 'failed'; -export type SupportingFileKey = - | 'gtfsGeolocationGeojson' - | 'gtfsDatasetRoutesJson'; -export interface SupportingFileDataMap { - gtfsGeolocationGeojson: GeoJSONData; - gtfsDatasetRoutesJson: GtfsRoute[]; -} -export interface SupportingFile< - K extends keyof SupportingFileDataMap = keyof SupportingFileDataMap, -> { - key: K; - status: LoadStatus; - url?: string; - data?: SupportingFileDataMap[K]; - error?: string; -}