diff --git a/eslint.config.mjs b/eslint.config.mjs index aa94178c..88e13f44 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -41,12 +41,6 @@ export default tseslint.config( ignoreRestSiblings: true, }, ], - // TODO: hooks called inside render callbacks, incorrect hook usage patterns, - // and missing/extra effect dependencies — to be fixed in a separate ticket. - 'react-hooks/rules-of-hooks': 'off', - 'react-hooks/refs': 'off', - 'react-hooks/set-state-in-render': 'off', - 'react-hooks/set-state-in-effect': 'off', 'react-hooks/immutability': 'off', 'react-hooks/exhaustive-deps': 'off', // TypeScript handles these; disable the core JS versions. @@ -61,10 +55,15 @@ export default tseslint.config( '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/no-unsafe-enum-comparison': 'off', - '@typescript-eslint/no-unnecessary-type-assertion': 'off', - '@typescript-eslint/no-redundant-type-constituents': 'off', + }, + }, + { + // Test files: `async` test callbacks and `act(async () => {})` wrappers are + // idiomatic even when they contain no `await` (async `act` flushes the + // microtask queue). Relaxing require-await here avoids churn in test infra. + files: ['**/*.spec.{ts,tsx}', '**/*.test.{ts,tsx}'], + rules: { '@typescript-eslint/require-await': 'off', - '@typescript-eslint/no-misused-promises': 'off', }, }, prettierRecommended, diff --git a/src/app/[locale]/account/AccountGeneral.tsx b/src/app/[locale]/account/AccountGeneral.tsx index ea469f56..1d70c994 100644 --- a/src/app/[locale]/account/AccountGeneral.tsx +++ b/src/app/[locale]/account/AccountGeneral.tsx @@ -42,6 +42,9 @@ export default function AccountGeneral(): React.ReactElement { draftIsRegisteredToReceiveAPIAnnouncements, setDraftIsRegisteredToReceiveAPIAnnouncements, ] = React.useState(false); + const [alertSeverity, setAlertSeverity] = React.useState<'success' | 'error'>( + 'success', + ); const handleEditClick = (): void => { setDraftFullName(user?.fullName ?? ''); @@ -76,14 +79,13 @@ export default function AccountGeneral(): React.ReactElement { React.useEffect(() => { if (saveStatus === 'success') { setIsEditing(false); + setAlertSeverity('success'); + } else if (saveStatus === 'fail') { + setAlertSeverity('error'); } }, [saveStatus]); - // Reference is due to dispatch save status acting faster than the exit animation of the alert, causing a flash of the wrong alert severity. With this reference, the severity will be consistent during the whole display of the alert. const isSaving = saveStatus === 'loading'; - const alertSeverity = React.useRef<'success' | 'error'>('success'); - if (saveStatus === 'success') alertSeverity.current = 'success'; - if (saveStatus === 'fail') alertSeverity.current = 'error'; return ( <> @@ -96,15 +98,13 @@ export default function AccountGeneral(): React.ReactElement { anchorOrigin={{ vertical: 'top', horizontal: 'center' }} > { dispatch(saveUserProfileReset()); }} sx={{ width: '100%' }} > - {alertSeverity.current === 'success' - ? t('saveSuccess') - : t('saveError')} + {alertSeverity === 'success' ? t('saveSuccess') : t('saveError')} { + onSubmit: (values) => { if (user != null) { dispatch( refreshUserInformation({ diff --git a/src/app/[locale]/contribute/FeedSubmission/Form/FirstStep.tsx b/src/app/[locale]/contribute/FeedSubmission/Form/FirstStep.tsx index a203f336..35893604 100644 --- a/src/app/[locale]/contribute/FeedSubmission/Form/FirstStep.tsx +++ b/src/app/[locale]/contribute/FeedSubmission/Form/FirstStep.tsx @@ -107,7 +107,11 @@ export default function FormFirstStep({ return ( <> -
+ { + void handleSubmit(onSubmit)(e); + }} + > - + { + void handleSubmit(onSubmit)(e); + }} + > diff --git a/src/app/[locale]/contribute/FeedSubmission/Form/SecondStep.tsx b/src/app/[locale]/contribute/FeedSubmission/Form/SecondStep.tsx index 76fccaa4..2e5ba37f 100644 --- a/src/app/[locale]/contribute/FeedSubmission/Form/SecondStep.tsx +++ b/src/app/[locale]/contribute/FeedSubmission/Form/SecondStep.tsx @@ -59,7 +59,11 @@ export default function FormSecondStep({ return ( <> {t('gtfsScheduleFeed')} - + { + void handleSubmit(onSubmit)(e); + }} + > {t('gtfsRealtimeFeed')} - + { + void handleSubmit(onSubmit)(e); + }} + > - + { + void handleSubmit(onSubmit)(e); + }} + > {/* Show required emptyLicenseUsage if official producer and no license provided */} {isOfficialProducer && noLicenseProvided && ( diff --git a/src/app/[locale]/feeds/[feedDataType]/[feedId]/static/layout.tsx b/src/app/[locale]/feeds/[feedDataType]/[feedId]/static/layout.tsx index 2beedda2..7d92a7dd 100644 --- a/src/app/[locale]/feeds/[feedDataType]/[feedId]/static/layout.tsx +++ b/src/app/[locale]/feeds/[feedDataType]/[feedId]/static/layout.tsx @@ -24,9 +24,9 @@ interface Props { * from the clean URLs like /feeds/gtfs/mdb-123. * TODO: Now that legacy catch-all route is removed, change this to a private route `_static` and update proxy and links accordingly. */ -export default async function StaticFeedLayout({ +export default function StaticFeedLayout({ children, params, -}: Props): Promise { +}: Props): React.ReactElement { return <>{children}; } diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 03610522..93b769e0 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -84,7 +84,7 @@ export default async function LocaleLayout({ } // At this point, locale is guaranteed to be a valid Locale type - const validLocale = locale as Locale; + const validLocale = locale; // Enable static rendering for this locale setRequestLocale(validLocale); diff --git a/src/app/[locale]/metrics/_components/Analytics/GBFSFeedAnalytics/DetailPanel.tsx b/src/app/[locale]/metrics/_components/Analytics/GBFSFeedAnalytics/DetailPanel.tsx index d04b36f1..82fe022f 100644 --- a/src/app/[locale]/metrics/_components/Analytics/GBFSFeedAnalytics/DetailPanel.tsx +++ b/src/app/[locale]/metrics/_components/Analytics/GBFSFeedAnalytics/DetailPanel.tsx @@ -32,26 +32,6 @@ const DetailPanel: React.FC = ({ row }) => { const theme = useTheme(); const { notices, metrics } = row.original; - if (metrics == null) { - return
No metrics available
; - } - - const chartData = metrics.computed_on.map((date, index) => { - const utcDate = new Date(date).toLocaleDateString('en-CA', { - timeZone: 'UTC', - }); // Converts the date to UTC - - return { - date: utcDate, - count: metrics.errors_count[index], - }; - }); - - const domain = [ - new Date(chartData[0]?.date ?? '').getTime(), - new Date().getTime(), - ]; - // Define the columns for the notices table const columns = useMemo>>( () => [ @@ -91,6 +71,26 @@ const DetailPanel: React.FC = ({ row }) => { muiTableContainerProps: { sx: { maxHeight: '50vh' } }, }); + if (metrics == null) { + return
No metrics available
; + } + + const chartData = metrics.computed_on.map((date, index) => { + const utcDate = new Date(date).toLocaleDateString('en-CA', { + timeZone: 'UTC', + }); // Converts the date to UTC + + return { + date: utcDate, + count: metrics.errors_count[index], + }; + }); + + const domain = [ + new Date(chartData[0]?.date ?? '').getTime(), + new Date().getTime(), + ]; + return ( diff --git a/src/app/[locale]/metrics/_components/Analytics/GBFSFeedAnalytics/GBFSFeedAnalytics.tsx b/src/app/[locale]/metrics/_components/Analytics/GBFSFeedAnalytics/GBFSFeedAnalytics.tsx index f25af323..846cffbb 100644 --- a/src/app/[locale]/metrics/_components/Analytics/GBFSFeedAnalytics/GBFSFeedAnalytics.tsx +++ b/src/app/[locale]/metrics/_components/Analytics/GBFSFeedAnalytics/GBFSFeedAnalytics.tsx @@ -46,15 +46,16 @@ import { setAnalyticsBucketEndpoint } from '../../../../../utils/metricsUtils'; export default function GBFSFeedAnalytics(): React.ReactElement { const searchParams = useSearchParams(); const { config } = useRemoteConfig(); - const [schemaPathFilters, setSchemaPathFilters] = React.useState( - [], - ); const versionFilter = searchParams.get('version'); const schemaPathInitFilter = decodeURIComponent( searchParams.get('schemaPath') ?? '', ); + const [schemaPathFilters, setSchemaPathFilters] = React.useState( + schemaPathInitFilter ? [schemaPathInitFilter] : [], + ); + const dispatch = useDispatch(); const rawData = useSelector(selectGBFSFeedMetrics); const status = useSelector(selectGBFSAnalyticsStatus); @@ -126,15 +127,6 @@ export default function GBFSFeedAnalytics(): React.ReactElement { return filters; }, [versionFilter]); - useMemo(() => { - if ( - schemaPathInitFilter != null && - schemaPathFilterOptions.includes(schemaPathInitFilter) - ) { - setSchemaPathFilters([schemaPathInitFilter]); - } - }, [schemaPathInitFilter, schemaPathFilterOptions]); - const columns = useTableColumns(); const csvConfig = mkConfig({ fieldSeparator: ',', diff --git a/src/app/[locale]/metrics/_components/Analytics/GBFSNoticeAnalytics/GBFSNoticeAnalytics.tsx b/src/app/[locale]/metrics/_components/Analytics/GBFSNoticeAnalytics/GBFSNoticeAnalytics.tsx index 1aefd028..f57f994d 100644 --- a/src/app/[locale]/metrics/_components/Analytics/GBFSNoticeAnalytics/GBFSNoticeAnalytics.tsx +++ b/src/app/[locale]/metrics/_components/Analytics/GBFSNoticeAnalytics/GBFSNoticeAnalytics.tsx @@ -32,6 +32,7 @@ export default function GBFSNoticeAnalytics(): React.ReactElement { const router = useRouter(); const searchParams = useSearchParams(); const noticeCode = searchParams.get('noticeCode'); + const theme = useTheme(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -133,7 +134,6 @@ export default function GBFSNoticeAnalytics(): React.ReactElement { enableStickyFooter: true, muiTableContainerProps: { sx: { maxHeight: '70vh' } }, renderDetailPanel: ({ row }) => { - const theme = useTheme(); const metrics = row.original; const chartData = metrics.computed_on.map((date, index) => ({ diff --git a/src/app/[locale]/metrics/_components/Analytics/GBFSVersionAnalytics/GBFSVersionAnalytics.tsx b/src/app/[locale]/metrics/_components/Analytics/GBFSVersionAnalytics/GBFSVersionAnalytics.tsx index 2ec10c29..17a208e0 100644 --- a/src/app/[locale]/metrics/_components/Analytics/GBFSVersionAnalytics/GBFSVersionAnalytics.tsx +++ b/src/app/[locale]/metrics/_components/Analytics/GBFSVersionAnalytics/GBFSVersionAnalytics.tsx @@ -39,6 +39,7 @@ import { GBFS_LINK } from '../../../../../constants/Navigation'; export default function GBFSVersionAnalytics(): React.ReactElement { const router = useRouter(); + const theme = useTheme(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -157,7 +158,6 @@ export default function GBFSVersionAnalytics(): React.ReactElement { enableStickyFooter: true, muiTableContainerProps: { sx: { maxHeight: '70vh' } }, renderDetailPanel: ({ row }) => { - const theme = useTheme(); const metrics = row.original; const chartData = metrics.computed_on.map((date, index) => ({ diff --git a/src/app/[locale]/metrics/_components/Analytics/GTFSFeatureAnalytics/GTFSFeatureAnalytics.tsx b/src/app/[locale]/metrics/_components/Analytics/GTFSFeatureAnalytics/GTFSFeatureAnalytics.tsx index 0e4260f3..b4525eb8 100644 --- a/src/app/[locale]/metrics/_components/Analytics/GTFSFeatureAnalytics/GTFSFeatureAnalytics.tsx +++ b/src/app/[locale]/metrics/_components/Analytics/GTFSFeatureAnalytics/GTFSFeatureAnalytics.tsx @@ -41,6 +41,7 @@ import { export default function GTFSFeatureAnalytics(): React.ReactElement { const router = useRouter(); + const theme = useTheme(); const searchParams = useSearchParams(); const featureName = searchParams.get('featureName'); const [data, setData] = useState([]); @@ -221,7 +222,6 @@ export default function GTFSFeatureAnalytics(): React.ReactElement { enableStickyFooter: true, muiTableContainerProps: { sx: { maxHeight: '70vh' } }, renderDetailPanel: ({ row }) => { - const theme = useTheme(); const metrics = row.original; const chartData = metrics.computed_on.map((date, index) => ({ diff --git a/src/app/[locale]/metrics/_components/Analytics/GTFSNoticeAnalytics/GTFSNoticeAnalytics.tsx b/src/app/[locale]/metrics/_components/Analytics/GTFSNoticeAnalytics/GTFSNoticeAnalytics.tsx index 5c003a8a..8019eeaf 100644 --- a/src/app/[locale]/metrics/_components/Analytics/GTFSNoticeAnalytics/GTFSNoticeAnalytics.tsx +++ b/src/app/[locale]/metrics/_components/Analytics/GTFSNoticeAnalytics/GTFSNoticeAnalytics.tsx @@ -39,6 +39,7 @@ import { useRemoteConfig } from '../../../../../context/RemoteConfigProvider'; export default function GTFSNoticeAnalytics(): React.ReactElement { const router = useRouter(); + const theme = useTheme(); const searchParams = useSearchParams(); const noticeCode = searchParams.get('noticeCode'); const [data, setData] = useState([]); @@ -193,7 +194,6 @@ export default function GTFSNoticeAnalytics(): React.ReactElement { enableStickyFooter: true, muiTableContainerProps: { sx: { maxHeight: '70vh' } }, renderDetailPanel: ({ row }) => { - const theme = useTheme(); const metrics = row.original; const chartData = metrics.computed_on.map((date, index) => ({ diff --git a/src/app/api/revalidate/route.spec.ts b/src/app/api/revalidate/route.spec.ts index 47a1cd6d..38e60b37 100644 --- a/src/app/api/revalidate/route.spec.ts +++ b/src/app/api/revalidate/route.spec.ts @@ -43,7 +43,7 @@ describe('GET /api/revalidate', () => { }, }); - const response = await GET(request); + const response = GET(request); const json = await response.json(); expect(response.status).toBe(500); @@ -65,7 +65,7 @@ describe('GET /api/revalidate', () => { }, }); - const response = await GET(request); + const response = GET(request); const json = await response.json(); expect(response.status).toBe(401); @@ -87,7 +87,7 @@ describe('GET /api/revalidate', () => { }, }); - const response = await GET(request); + const response = GET(request); const json = await response.json(); expect(response.status).toBe(200); diff --git a/src/app/api/revalidate/route.ts b/src/app/api/revalidate/route.ts index 1ffa841d..b004953e 100644 --- a/src/app/api/revalidate/route.ts +++ b/src/app/api/revalidate/route.ts @@ -36,7 +36,7 @@ const defaultRevalidateOptions: RevalidateBody = { * Vercel automatically passes Authorization: Bearer with each invocation. * Configured in vercel.json under "crons" for 4am UTC Monday-Saturday and 7am UTC Sunday. */ -export async function GET(req: Request): Promise { +export function GET(req: Request): NextResponse { const authHeader = req.headers.get('authorization'); const cronSecret = process.env.CRON_SECRET; diff --git a/src/app/api/session/route.ts b/src/app/api/session/route.ts index 5869b609..ccc12a0c 100644 --- a/src/app/api/session/route.ts +++ b/src/app/api/session/route.ts @@ -62,7 +62,7 @@ export async function POST(req: NextRequest): Promise { } } -export async function GET(req: NextRequest): Promise { +export function GET(req: NextRequest): NextResponse { try { const cookie = req.cookies.get(COOKIE_NAME)?.value; if (cookie == null) { @@ -89,6 +89,7 @@ export async function GET(req: NextRequest): Promise { } export async function DELETE(req: NextRequest): Promise { + // Clear the session cookie so that subsequent requests have no session. const response = NextResponse.json({ status: 'logged_out' }); response.cookies.delete(COOKIE_NAME); return response; diff --git a/src/app/components/CoveredAreaMap.tsx b/src/app/components/CoveredAreaMap.tsx index 82f57e4d..d2ba2292 100644 --- a/src/app/components/CoveredAreaMap.tsx +++ b/src/app/components/CoveredAreaMap.tsx @@ -1,6 +1,7 @@ 'use client'; -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; +import useSWR from 'swr'; import { Box, ToggleButtonGroup, @@ -95,15 +96,6 @@ const CoveredAreaMap: React.FC = ({ const theme = useTheme(); const { config } = useRemoteConfig(); - const [geoJsonData, setGeoJsonData] = useState< - GeoJSONData | GeoJSONDataGBFS | null - >(null); - const [geoJsonError, setGeoJsonError] = useState(false); - const [geoJsonLoading, setGeoJsonLoading] = useState(false); - const [view, setView] = useState( - feed?.data_type === 'gtfs' ? 'gtfsVisualizationView' : 'boundingBoxView', - ); - const latestGbfsVersion = useMemo((): GBFSVersionType | undefined => { if (feed?.data_type !== 'gbfs') return undefined; return getLatestGbfsVersion(feed as GBFSFeedType); @@ -111,86 +103,89 @@ const CoveredAreaMap: React.FC = ({ const hasNoRoutes = totalRoutes == undefined || totalRoutes === 0; - const getAndSetGeoJsonData = (urlToExtract: string): void => { - setGeoJsonLoading(true); - fetchGeoJson(urlToExtract) - .then((data) => { - setGeoJsonData(data); - setGeoJsonError(false); - }) - .catch(() => { - setGeoJsonError(true); - }) - .finally(() => { - setGeoJsonLoading(false); - }); - }; - - useEffect(() => { + // Compute the URL to fetch GeoJSON from, or null if nothing to fetch + const geoJsonUrl = useMemo(() => { if (feed?.data_type === 'gbfs') { - const latestGbfsVersionReportUrl = + const reportUrl = latestGbfsVersion?.latest_validation_report?.report_summary_url; - if ( - !config.enableDetailedCoveredArea || - latestGbfsVersionReportUrl === undefined - ) { - setGeoJsonData(null); - setGeoJsonError(config.enableDetailedCoveredArea); - return; - } - getAndSetGeoJsonData(latestGbfsVersionReportUrl); - return; + if (!config.enableDetailedCoveredArea || reportUrl == undefined) + return null; + return reportUrl; } if ( feed?.data_type === 'gtfs' && - latestDataset?.hosted_url != undefined && - boundingBox != undefined && + latestDataset?.hosted_url != null && + boundingBox != null && config.enableDetailedCoveredArea ) { - getAndSetGeoJsonData(latestDataset.hosted_url); - return; + return latestDataset.hosted_url; } - setGeoJsonData(null); - setGeoJsonError(config.enableDetailedCoveredArea); - }, [latestDataset, feed, config.enableDetailedCoveredArea]); + return null; + }, [ + feed, + latestGbfsVersion, + latestDataset, + boundingBox, + config.enableDetailedCoveredArea, + ]); + + const { + data: geoJsonData, + error: geoJsonError, + isLoading: geoJsonLoading, + } = useSWR(geoJsonUrl, fetchGeoJson, { revalidateOnFocus: false }); + + // For GBFS, the fetched data may still lack a computable bounding box + const gbfsGeoJsonBoundingBox = + feed?.data_type === 'gbfs' && geoJsonData != null + ? (computeBoundingBox(geoJsonData) ?? []) + : []; + const geoJsonFailed = + !!geoJsonError || + (feed?.data_type === 'gbfs' && + geoJsonData != null && + gbfsGeoJsonBoundingBox.length === 0); - // effect to determine which view to display - useEffect(() => { - if (feed == undefined) return; + // Derive the default view from current props and loaded data + const computedView = useMemo((): MapViews => { if (feed?.data_type === 'gbfs') { - setView( - config.enableDetailedCoveredArea - ? 'detailedCoveredAreaView' - : 'boundingBoxView', - ); - return; + return config.enableDetailedCoveredArea + ? 'detailedCoveredAreaView' + : 'boundingBoxView'; } - - // for gtfs feeds - if ( - feed?.data_type === 'gtfs' && - !hasNoRoutes && - boundingBox != undefined - ) { - setView('gtfsVisualizationView'); - return; + if (feed?.data_type === 'gtfs' && !hasNoRoutes && boundingBox != null) { + return 'gtfsVisualizationView'; } if ( config.enableDetailedCoveredArea && geoJsonData != null && - boundingBox != undefined + boundingBox != null ) { - setView('detailedCoveredAreaView'); - return; + return 'detailedCoveredAreaView'; } - setView('boundingBoxView'); - }, [feed, totalRoutes, boundingBox, geoJsonData]); + return 'boundingBoxView'; + }, [ + feed?.data_type, + hasNoRoutes, + boundingBox, + geoJsonData, + config.enableDetailedCoveredArea, + ]); + + // Track an explicit user view selection per feed so it resets on navigation + const [userViewState, setUserViewState] = useState<{ + feedId: string; + view: MapViews; + } | null>(null); + const view = + userViewState?.feedId === feed?.id ? userViewState?.view : computedView; const handleViewChange = ( _: React.MouseEvent, newView: MapViews | null, ): void => { - if (newView !== null) setView(newView); + if (newView !== null) + setUserViewState({ feedId: feed?.id ?? '', view: newView }); }; const handleOpenDetailedMapClick = (): void => { @@ -255,17 +250,13 @@ const CoveredAreaMap: React.FC = ({ ); } if (config.enableDetailedCoveredArea && geoJsonData != null) { - let gbfsGeoJsonBoundingBox: LngLatTuple[] = []; - if (feed?.data_type === 'gbfs') { - gbfsGeoJsonBoundingBox = computeBoundingBox(geoJsonData) ?? []; - if (gbfsGeoJsonBoundingBox.length === 0) { - setGeoJsonError(true); - } - } const feedBoundingBox: LngLatTuple[] = feed?.data_type === 'gtfs' ? (boundingBox ?? []) : gbfsGeoJsonBoundingBox; + if (feed?.data_type === 'gbfs' && gbfsGeoJsonBoundingBox.length === 0) { + return <>; + } return ( = ({ variant='subtitle1' sx={{ color: 'text.secondary', mt: 0.5 }} > - {t('coveredAreaTitle') + ' - ' + t(view)} + {t('coveredAreaTitle') + ' - ' + t(view ?? '')} {feed?.data_type === 'gbfs' && ( @@ -371,7 +362,8 @@ const CoveredAreaMap: React.FC = ({ value='detailedCoveredAreaView' disabled={ geoJsonLoading || - geoJsonError || + geoJsonFailed || + geoJsonUrl == null || boundingBox === undefined } aria-label='Detailed Covered Area View' @@ -402,11 +394,11 @@ const CoveredAreaMap: React.FC = ({ {config.enableDetailedCoveredArea && feed?.data_type === 'gbfs' && - geoJsonError && ( + (geoJsonFailed || geoJsonUrl == null) && ( {t('unableToGetGbfsMap')} )} - {(boundingBox != undefined || !geoJsonError) && ( + {(boundingBox != undefined || !geoJsonFailed) && ( {geoJsonLoading ? ( { describe('generateRouteOutlineColorExpression', () => { // Helper to safely index into nested unknown arrays - type NestedArr = Array; + type NestedArr = unknown[]; const at = (arr: NestedArr, ...indices: number[]): NestedArr => indices.reduce((a, i) => (a as NestedArr[])[i], arr); it('returns an array expression starting with "let" and "rgba"', () => { diff --git a/src/app/components/GtfsVisualizationMap.tsx b/src/app/components/GtfsVisualizationMap.tsx index d7752c05..a7041046 100644 --- a/src/app/components/GtfsVisualizationMap.tsx +++ b/src/app/components/GtfsVisualizationMap.tsx @@ -532,7 +532,6 @@ export const GtfsVisualizationMap = ({ scannedTiles={scannedTiles} scanRowsCols={scanRowsCols} handleCancelScan={handleCancelScan} - cancelRequestRef={cancelRequestRef} /> )} diff --git a/src/app/components/Map/ScanningOverlay.tsx b/src/app/components/Map/ScanningOverlay.tsx index 371d2ae6..09947b46 100644 --- a/src/app/components/Map/ScanningOverlay.tsx +++ b/src/app/components/Map/ScanningOverlay.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { Box, Button, @@ -16,19 +17,13 @@ interface ScanningOverlayProps { cols: number; } | null; handleCancelScan: () => void; - cancelRequestRef: React.MutableRefObject; } export const ScanningOverlay = ( props: React.PropsWithChildren, ): React.ReactElement => { - const { - totalTiles, - scannedTiles, - scanRowsCols, - handleCancelScan, - cancelRequestRef, - } = props; + const { totalTiles, scannedTiles, scanRowsCols, handleCancelScan } = props; + const [cancelClicked, setCancelClicked] = useState(false); const theme = useTheme(); const t = useTranslations('feeds'); const progressPct = @@ -121,8 +116,11 @@ export const ScanningOverlay = (