diff --git a/package-lock.json b/package-lock.json index ace6531..f9e244c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-favicon": "1.0.1", + "react-router-dom": "^6.21.1", "react-spring": "^8.0.27", "reakit": "^1.3.11", "seamless-scroll-polyfill": "^1.0.10", @@ -791,6 +792,14 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@remix-run/router": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.1.tgz", + "integrity": "sha512-Qg4DMQsfPNAs88rb2xkdk03N3bjK4jgX5fR24eHCTR9q6PrhZQZ4UJBPzCHJkIpTRN1UKxx2DzjZmnC+7Lj0Ow==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -5567,6 +5576,36 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-router": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.1.tgz", + "integrity": "sha512-W0l13YlMTm1YrpVIOpjCADJqEUpz1vm+CMo47RuFX4Ftegwm6KOYsL5G3eiE52jnJpKvzm6uB/vTKTPKM8dmkA==", + "dependencies": { + "@remix-run/router": "1.14.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.1.tgz", + "integrity": "sha512-QCNrtjtDPwHDO+AO21MJd7yIcr41UetYt5jzaB9Y1UYaPTCnVuJq6S748g1dE11OQlCFIQg+RtAA1SEZIyiBeA==", + "dependencies": { + "@remix-run/router": "1.14.1", + "react-router": "6.21.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-spring": { "version": "8.0.27", "resolved": "https://registry.npmjs.org/react-spring/-/react-spring-8.0.27.tgz", @@ -7855,6 +7894,11 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==" }, + "@remix-run/router": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.1.tgz", + "integrity": "sha512-Qg4DMQsfPNAs88rb2xkdk03N3bjK4jgX5fR24eHCTR9q6PrhZQZ4UJBPzCHJkIpTRN1UKxx2DzjZmnC+7Lj0Ow==" + }, "@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -11361,6 +11405,23 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-router": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.1.tgz", + "integrity": "sha512-W0l13YlMTm1YrpVIOpjCADJqEUpz1vm+CMo47RuFX4Ftegwm6KOYsL5G3eiE52jnJpKvzm6uB/vTKTPKM8dmkA==", + "requires": { + "@remix-run/router": "1.14.1" + } + }, + "react-router-dom": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.1.tgz", + "integrity": "sha512-QCNrtjtDPwHDO+AO21MJd7yIcr41UetYt5jzaB9Y1UYaPTCnVuJq6S748g1dE11OQlCFIQg+RtAA1SEZIyiBeA==", + "requires": { + "@remix-run/router": "1.14.1", + "react-router": "6.21.1" + } + }, "react-spring": { "version": "8.0.27", "resolved": "https://registry.npmjs.org/react-spring/-/react-spring-8.0.27.tgz", diff --git a/package.json b/package.json index cdda2e0..085b79e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-favicon": "1.0.1", + "react-router-dom": "^6.21.1", "react-spring": "^8.0.27", "reakit": "^1.3.11", "seamless-scroll-polyfill": "^1.0.10", diff --git a/src/components/AgeTabPicker.tsx b/src/components/AgeTabPicker.tsx index 486ccdd..fc2956b 100644 --- a/src/components/AgeTabPicker.tsx +++ b/src/components/AgeTabPicker.tsx @@ -1,6 +1,7 @@ import { useRef, useLayoutEffect } from 'react'; -import { TabList, Tab, TabStateReturn } from 'reakit'; +import { TabList, Tab } from 'reakit'; import { VehiclesAge } from '../types'; +import { useAgeSearchParam } from '../hooks/searchParams'; type TrainAge = { key: VehiclesAge; label: string }; @@ -11,11 +12,13 @@ const trainTypes: TrainAge[] = [ ]; interface AgeTabPickerProps { - tabState: TabStateReturn; tabColor: string; } -export const AgeTabPicker: React.FC = ({ tabState, tabColor }) => { +export const AgeTabPicker: React.FC = ({ tabColor }) => { + // Get train age ID from serach params + const [ageSearchParam, setAgeSearchParam] = useAgeSearchParam(); + const wrapperRef = useRef(null); const selectedIndicatorRef = useRef(null); @@ -23,48 +26,33 @@ export const AgeTabPicker: React.FC = ({ tabState, tabColor } const { current: wrapper } = wrapperRef; const { current: selectedIndicator } = selectedIndicatorRef; if (wrapper && selectedIndicator) { - const selectedEl = wrapper.querySelector( - `#${tabState.selectedId}` - ) as HTMLElement | null; + const selectedEl = wrapper.querySelector(`#${ageSearchParam}`) as HTMLElement | null; if (selectedEl) { selectedIndicator.style.width = selectedEl.clientWidth + 'px'; selectedIndicator.style.transform = `translateX(${selectedEl.offsetLeft}px)`; - selectedIndicator.style.transition = '500ms all cubic-bezier(0.86, 0, 0.07, 1)'; - } - } - }, [tabState.selectedId]); - - // Handle color change immediate transition - useLayoutEffect(() => { - const { current: wrapper } = wrapperRef; - const { current: selectedIndicator } = selectedIndicatorRef; - if (wrapper && selectedIndicator) { - const selectedEl = wrapper.querySelector( - `#${tabState.selectedId}` - ) as HTMLElement | null; - if (selectedEl) { selectedIndicator.style.backgroundColor = tabColor; - selectedIndicator.style.transition = '0ms background-color'; } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [tabColor]); + }, [tabColor, ageSearchParam]); return ( - +
+ {trainTypes.map((trainType) => { return ( { + setAgeSearchParam(trainType.key); + }} >
diff --git a/src/components/App.tsx b/src/components/App.tsx index e6bafbe..7aa2f47 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,6 +1,5 @@ import { useEffect, useLayoutEffect } from 'react'; import Favicon from 'react-favicon'; -import { useTabState } from 'reakit'; import { greenLine, orangeLine, redLine, blueLine } from '../lines'; import { useMbtaApi } from '../hooks/useMbtaApi'; @@ -9,30 +8,32 @@ import { getInitialDataByKey } from '../initialData'; import { Line } from './Line'; import { Header } from './Header'; import { Footer } from './Footer'; -import { LineTabPicker, getTabIdForLine } from './LineTabPicker'; +import { LineTabPicker } from './LineTabPicker'; import { LineStats } from './LineStats/LineStats'; import { setCssVariable } from './util'; // @ts-expect-error Favicon png seems to throw typescript error import favicon from '../../static/images/favicon.png'; import { AgeTabPicker } from './AgeTabPicker'; -import { Line as TLine, VehiclesAge } from '../types'; +import { Line as TLine } from '../types'; + +import { useSearchParams } from 'react-router-dom'; +import { useLineSearchParam, useAgeSearchParam } from '../hooks/searchParams'; const lineByTabId: Record = { - 'tab-Green': greenLine, - 'tab-Orange': orangeLine, - 'tab-Red': redLine, - 'tab-Blue': blueLine, + Green: greenLine, + Orange: orangeLine, + Red: redLine, + Blue: blueLine, }; export const App: React.FC = () => { - const tabState = useTabState({ loop: false }); - const ageTabState = useTabState({ currentId: 'new_vehicles', loop: false }); + const [searchParams] = useSearchParams(); + const [lineSearchParam, setLineSearchParam] = useLineSearchParam(); + const [ageSearchParam] = useAgeSearchParam(); - const api = useMbtaApi(Object.values(lineByTabId), ageTabState.currentId as VehiclesAge); - const selectedLine = tabState.currentId - ? lineByTabId[tabState.currentId] - : lineByTabId['tab-Green']; + const api = useMbtaApi(Object.values(lineByTabId), ageSearchParam); + const selectedLine = lineByTabId[lineSearchParam]; useLayoutEffect(() => { const backgroundColor = selectedLine.colorSecondary; @@ -41,17 +42,25 @@ export const App: React.FC = () => { }, [selectedLine]); useEffect(() => { - if (api.isReady) { - const lineWithTrains = Object.values(lineByTabId).find((line) => { - const routeIds = Object.keys(line.routes); - if (api.trainsByRoute !== null) { - // @ts-expect-error Despite the above check, Typescript fails to understand that trainsByRoute won't be null - return routeIds.some((routeId) => api.trainsByRoute[routeId].length > 0); - } - }); - if (lineWithTrains) { - tabState.setCurrentId(getTabIdForLine(lineWithTrains)); + // Do not override the line if a query string param exists + if (searchParams.get('line') !== null) { + return; + } + + // Do not run until api is ready + if (!api.isReady) { + return; + } + + const lineWithTrains = Object.values(lineByTabId).find((line) => { + const routeIds = Object.keys(line.routes); + if (api.trainsByRoute) { + return routeIds.some((routeId) => api.trainsByRoute![routeId].length > 0); } + }); + + if (lineWithTrains) { + setLineSearchParam(lineWithTrains); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [api.isReady]); @@ -66,17 +75,12 @@ export const App: React.FC = () => { return () => document.removeEventListener('keydown', listener); }, []); - const selectedLineColor: string = tabState.currentId - ? lineByTabId[tabState.currentId]?.color - : greenLine.color; - const renderControls = () => { return (
- + {api.trainsByRoute && ( @@ -90,12 +94,7 @@ export const App: React.FC = () => { <>
- +