diff --git a/messages/en.json b/messages/en.json index 09aa3467..54989ee1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -117,7 +117,7 @@ "resultsFor": "{startResult}-{endResult} of {totalResults} results", "deprecated": "Deprecated", "searchPlaceholder": "Transit provider, feed name, or location", - "noResults": "We're sorry, we found no search results for '{activeSearch}'.", + "noResults": "We're sorry, we found no search results for ''{activeSearch}''.", "searchSuggestions": "Search suggestions: ", "searchTips": { "twoDigit": "Use the full English name of a location e.g \"France\" or \"New York City\"", diff --git a/messages/fr.json b/messages/fr.json index 610c3162..4c5ce805 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -117,7 +117,7 @@ "resultsFor": "{startResult}-{endResult} of {totalResults} results", "deprecated": "Deprecated", "searchPlaceholder": "Transit provider, feed name, or location", - "noResults": "We're sorry, we found no search results for '{activeSearch}'.", + "noResults": "We're sorry, we found no search results for ''{activeSearch}''.", "searchSuggestions": "Search suggestions: ", "searchTips": { "twoDigit": "Use the full English name of a location e.g \"France\" or \"New York City\"", diff --git a/src/app/api/revalidate/route.ts b/src/app/api/revalidate/route.ts index 70340c5c..d04f9b71 100644 --- a/src/app/api/revalidate/route.ts +++ b/src/app/api/revalidate/route.ts @@ -22,6 +22,51 @@ const defaultRevalidateOptions: RevalidateBody = { feedIds: [], }; +/** + * GET handler for the Vercel cron job that revalidates all feed pages once a day. + * Vercel automatically passes Authorization: Bearer with each invocation. + * Configured in vercel.json under "crons" (schedule: 0 9 * * * = 4am EST / 9am UTC). + */ +export async function GET(req: Request): Promise { + const authHeader = req.headers.get('authorization'); + const cronSecret = process.env.CRON_SECRET; + + if (cronSecret == null) { + return NextResponse.json( + { ok: false, error: 'Server misconfigured: CRON_SECRET missing' }, + { status: 500 }, + ); + } + + if (authHeader !== `Bearer ${cronSecret}`) { + return NextResponse.json( + { ok: false, error: 'Unauthorized' }, + { status: 401 }, + ); + } + + try { + revalidateTag('guest-feeds', 'max'); + revalidatePath('/[locale]/feeds/[feedDataType]/[feedId]', 'layout'); + console.log( + '[cron] revalidate /api/revalidate: all-feeds revalidation triggered', + ); + return NextResponse.json({ + ok: true, + message: 'All feeds revalidated successfully', + }); + } catch (error) { + console.error( + '[cron] revalidate /api/revalidate: revalidation failed:', + error, + ); + return NextResponse.json( + { ok: false, error: 'Revalidation failed' }, + { status: 500 }, + ); + } +} + export async function POST(req: Request): Promise { const expectedSecret = nonEmpty(process.env.REVALIDATE_SECRET); if (expectedSecret == null) { diff --git a/src/app/components/Header.tsx b/src/app/components/Header.tsx index d240a2b6..13244d26 100644 --- a/src/app/components/Header.tsx +++ b/src/app/components/Header.tsx @@ -438,6 +438,7 @@ export default function DrawerAppBar(): React.ReactElement { diff --git a/src/app/screens/Feed/Feed.functions.tsx b/src/app/screens/Feed/Feed.functions.tsx index 9013a38a..a73cceeb 100644 --- a/src/app/screens/Feed/Feed.functions.tsx +++ b/src/app/screens/Feed/Feed.functions.tsx @@ -3,6 +3,7 @@ import { Box, Typography } from '@mui/material'; import { type GTFSFeedType, type GBFSVersionType, + type GBFSFeedType, } from '../../services/feeds/utils'; import { type LatLngTuple } from 'leaflet'; import { type GeoJSONData, type GeoJSONDataGBFS } from '../../types'; @@ -192,36 +193,35 @@ export function computeBoundingBox( } export const getBoundingBox = ( - feed: GTFSFeedType, + feed: GTFSFeedType | GBFSFeedType , ): LatLngTuple[] | undefined => { - if (feed == undefined || feed.data_type !== 'gtfs') { + if (feed == undefined || feed.data_type === 'gtfs_rt') { return undefined; - } - const gtfsFeed: GTFSFeedType = feed; + }; if ( - gtfsFeed.bounding_box?.maximum_latitude == undefined || - gtfsFeed.bounding_box?.maximum_longitude == undefined || - gtfsFeed.bounding_box?.minimum_latitude == undefined || - gtfsFeed.bounding_box?.minimum_longitude == undefined + 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 [ [ - gtfsFeed.bounding_box.minimum_latitude, - gtfsFeed.bounding_box.minimum_longitude, + feed.bounding_box.minimum_latitude, + feed.bounding_box.minimum_longitude, ], [ - gtfsFeed.bounding_box.minimum_latitude, - gtfsFeed.bounding_box.maximum_longitude, + feed.bounding_box.minimum_latitude, + feed.bounding_box.maximum_longitude, ], [ - gtfsFeed.bounding_box.maximum_latitude, - gtfsFeed.bounding_box.maximum_longitude, + feed.bounding_box.maximum_latitude, + feed.bounding_box.maximum_longitude, ], [ - gtfsFeed.bounding_box.maximum_latitude, - gtfsFeed.bounding_box.minimum_longitude, + feed.bounding_box.maximum_latitude, + feed.bounding_box.minimum_longitude, ], ]; }; diff --git a/src/app/screens/Feed/FeedView.tsx b/src/app/screens/Feed/FeedView.tsx index a5d36684..4fecc2e1 100644 --- a/src/app/screens/Feed/FeedView.tsx +++ b/src/app/screens/Feed/FeedView.tsx @@ -128,7 +128,7 @@ export default async function FeedView({ ? (feed as GBFSFeedType)?.source_info?.producer_url : undefined; // Simplified - const boundingBox = getBoundingBox(feed as GTFSFeedType); + const boundingBox = getBoundingBox(feed); let latestDataset: LatestDatasetFull; if (feed.data_type === 'gtfs') { diff --git a/src/app/screens/Feed/components/FeedNavigationControls.tsx b/src/app/screens/Feed/components/FeedNavigationControls.tsx index 8b6992dc..04c8b5ba 100644 --- a/src/app/screens/Feed/components/FeedNavigationControls.tsx +++ b/src/app/screens/Feed/components/FeedNavigationControls.tsx @@ -3,7 +3,7 @@ import { Button, Grid, Typography } from '@mui/material'; import { ChevronLeft } from '@mui/icons-material'; import { useTranslations } from 'next-intl'; -import { useRouter } from '../../../../i18n/navigation'; +import { Link, useRouter } from '../../../../i18n/navigation'; interface Props { feedDataType: string; @@ -43,12 +43,18 @@ export default function FeedNavigationControls({ }, }} > - /