Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save state on refresh, use URL state #169

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
40 changes: 14 additions & 26 deletions src/components/AgeTabPicker.tsx
Original file line number Diff line number Diff line change
@@ -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 };

Expand All @@ -11,60 +12,47 @@ const trainTypes: TrainAge[] = [
];

interface AgeTabPickerProps {
tabState: TabStateReturn;
tabColor: string;
}

export const AgeTabPicker: React.FC<AgeTabPickerProps> = ({ tabState, tabColor }) => {
export const AgeTabPicker: React.FC<AgeTabPickerProps> = ({ tabColor }) => {
// Get train age ID from serach params
const [ageSearchParam, setAgeSearchParam] = useAgeSearchParam();

const wrapperRef = useRef<HTMLDivElement>(null);
const selectedIndicatorRef = useRef<HTMLDivElement>(null);

useLayoutEffect(() => {
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 (
<TabList {...tabState} className="tab-picker" aria-label="Select a line" ref={wrapperRef}>
<TabList className="tab-picker" aria-label="Select train age" ref={wrapperRef}>
<div className="selected-indicator" ref={selectedIndicatorRef} />

{trainTypes.map((trainType) => {
return (
<Tab
{...tabState}
id={trainType.key}
className="tab"
key={trainType.key}
as="div"
data-color={tabColor}
onClick={() => {
setAgeSearchParam(trainType.key);
}}
>
<div
aria-label={trainType.label}
aria-label={trainType.key}
className="icon age"
style={{ backgroundColor: tabColor }}
>
Expand Down
69 changes: 34 additions & 35 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<string, TLine> = {
'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;
Expand All @@ -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]);
Expand All @@ -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 (
<div className={'selectors'}>
<AgeTabPicker tabState={ageTabState} tabColor={selectedLineColor} />
<AgeTabPicker tabColor={selectedLine.color} />
{api.trainsByRoute && (
<LineTabPicker
tabState={tabState}
lines={Object.values(lineByTabId)}
trainsByRoute={api.trainsByRoute}
/>
Expand All @@ -90,12 +94,7 @@ export const App: React.FC = () => {
<>
<Favicon url={favicon} />
<Header controls={renderControls()} />
<Line
key={selectedLine?.name}
line={selectedLine}
api={api}
age={ageTabState.currentId ?? 'vehicles'}
/>
<Line key={selectedLine?.name} line={selectedLine} api={api} age={ageSearchParam} />
<LineStats line={selectedLine?.name} />
<Footer version={getInitialDataByKey('version')} />
</>
Expand Down
24 changes: 12 additions & 12 deletions src/components/LineTabPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { useRef, useLayoutEffect } from 'react';
import { TabList, Tab, TabStateReturn } from 'reakit';
import { TabList, Tab } from 'reakit';
import { Line, Train } from '../types';

import { getTrainRoutePairsForLine } from './util';

export const getTabIdForLine = (line: Line) => `tab-${line.name}`;
import { useLineSearchParam } from '../hooks/searchParams';

interface LineTabPickerProps {
lines: Line[];
tabState: TabStateReturn;
trainsByRoute: { [key: string]: Train[] };
}

export const LineTabPicker: React.FC<LineTabPickerProps> = ({ lines, tabState, trainsByRoute }) => {
export const LineTabPicker: React.FC<LineTabPickerProps> = ({ lines, trainsByRoute }) => {
const [lineSearchParam, setLineSearchParam] = useLineSearchParam();

const wrapperRef = useRef<HTMLDivElement>(null);
const selectedIndicatorRef = useRef<HTMLDivElement>(null);

Expand All @@ -23,9 +23,7 @@ export const LineTabPicker: React.FC<LineTabPickerProps> = ({ lines, tabState, t
const { current: wrapper } = wrapperRef;
const { current: selectedIndicator } = selectedIndicatorRef;
if (wrapper && selectedIndicator) {
const selectedEl = wrapper.querySelector(
`#${tabState.selectedId}`
) as HTMLElement | null;
const selectedEl = wrapper.querySelector(`#${lineSearchParam}`) as HTMLElement | null;
if (selectedEl) {
selectedIndicator.style.width = selectedEl.clientWidth + 'px';
selectedIndicator.style.transform = `translateX(${selectedEl.offsetLeft}px)`;
Expand All @@ -34,21 +32,23 @@ export const LineTabPicker: React.FC<LineTabPickerProps> = ({ lines, tabState, t
) as string;
}
}
}, [tabState.selectedId, totalTrainCount]);
}, [lineSearchParam, totalTrainCount]);

return (
<TabList {...tabState} className="tab-picker" aria-label="Select a line" ref={wrapperRef}>
<TabList className="tab-picker" aria-label="Select a line" ref={wrapperRef}>
<div className="selected-indicator" ref={selectedIndicatorRef} />
{lines.map((line) => {
const trains = getTrainRoutePairsForLine(trainsByRoute, line.routes);
return (
<Tab
{...tabState}
id={getTabIdForLine(line)}
id={line.name}
className="tab"
key={line.name}
as="div"
data-color={line.color}
onClick={() => {
setLineSearchParam(line);
}}
>
<div
aria-label={line.name + ' line'}
Expand Down
Loading
Loading