Skip to content

Commit 6f9e0cd

Browse files
committed
Move date manipulation out of client
1 parent 37514ad commit 6f9e0cd

20 files changed

+1719
-4379
lines changed

.eslintrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
},
3131
"settings": {
3232
"react": {
33-
"version": "detect"
33+
"version": "16.13.1"
3434
}
3535
}
3636
}

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
22

3+
deploy.md
4+
35
# webserver
46
node_modules
57
coverage

ROADMAP.md

+13-17
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,19 @@
22

33
## Next Steps
44

5-
- [] Run through [Express best practices](http://expressjs.com/en/advanced/best-practice-performance.html) and implement other improvements for production server
6-
- [] Run through [a11y project checklist](https://a11yproject.com/checklist/) to identify/address gaps in accessibility (Lighthouse audit score is 92 mobile, 98 web)
7-
- [] BUG: Handle singular labels ("1 rides")
8-
- [] BUG: Insert ride "day of week" meta in server to avoid browser specific date math
9-
- [] Show day of week in event list to instill trust in filter
10-
- [] Improve map performance, it's too slow
11-
- [] Make navbar fixed at top of page
12-
- [] Scroll to event in list when click on map marker
13-
- [] Add "clear/reset" filters
14-
- [] Add filters to controls: organizer, audience, location name, area (PDX/Vancouver), cancelled, duration, time of day
15-
- [] Add toggle: sort by distance vs date
16-
- [x] Add "use my location" toggle instead of asking for location on page load
17-
- [] Only display distance when location enabled
18-
- [] Make mapCenter draggable and/or drag map to filter
19-
- [] Add filter to map: expandable radius
20-
- [] Add button: report a problem
21-
- [] Merge duplicates (repeating rides, example: TNR every thursday)
5+
- Run through [Express best practices](http://expressjs.com/en/advanced/best-practice-performance.html) and implement other improvements for production server
6+
- Run through [a11y project checklist](https://a11yproject.com/checklist/) to identify/address gaps in accessibility (Lighthouse audit score is 92 mobile, 98 web)
7+
- Show day of week in event list to instill trust in filter
8+
- Improve map performance, it's too slow
9+
- Make navbar fixed at top of page
10+
- Scroll to event in list when click on map marker
11+
- Add "clear/reset" filters
12+
- Add filters to controls: organizer, audience, location name, area (PDX/Vancouver), cancelled, duration, time of day
13+
- Add toggle: sort by distance vs date
14+
- Make mapCenter draggable and/or drag map to filter
15+
- Add filter to map: expandable radius
16+
- Add button: report a problem
17+
- Merge duplicates (repeating rides, example: TNR every thursday)
2218

2319
## Cool ideas for later
2420

client/package-lock.json

+1,419-3,171
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
]
1616
},
1717
"dependencies": {
18-
"@fortawesome/fontawesome-svg-core": "^1.2.29",
19-
"@fortawesome/free-solid-svg-icons": "^5.13.1",
18+
"@fortawesome/fontawesome-svg-core": "^1.2.30",
19+
"@fortawesome/free-solid-svg-icons": "^5.14.0",
2020
"@fortawesome/react-fontawesome": "^0.1.11",
2121
"@testing-library/jest-dom": "^4.2.4",
2222
"@testing-library/react": "^9.5.0",
@@ -28,14 +28,14 @@
2828
"react-dom": "^16.13.1",
2929
"react-leaflet": "^2.7.0",
3030
"react-router-dom": "^5.2.0",
31-
"react-scripts": "3.4.0",
32-
"typescript": "^3.9.6"
31+
"react-scripts": "^3.4.3",
32+
"typescript": "^3.9.7"
3333
},
3434
"devDependencies": {
3535
"@types/jest": "^24.9.1",
36-
"@types/leaflet": "^1.5.15",
37-
"@types/node": "^12.12.48",
38-
"@types/react": "^16.9.41",
36+
"@types/leaflet": "^1.5.17",
37+
"@types/node": "^12.12.54",
38+
"@types/react": "^16.9.48",
3939
"@types/react-dom": "^16.9.8",
4040
"@types/react-leaflet": "^2.5.2",
4141
"@types/react-router-dom": "^5.1.5"

client/src/components/Controls.test.tsx

+10-17
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ function mockFunc(): void {
99

1010
test('renders the start input field', () => {
1111
const { getByLabelText } = render(
12-
<Controls data={{ events: [], start: '', end: '' }} handleEventsFiltered={mockFunc} />
12+
<Controls data={{ events: [], start: '', end: '' }} updateMapCenter={mockFunc} handleEventsFiltered={mockFunc} />
1313
);
1414
const startDatePicker = getByLabelText(/from/i);
1515
expect(startDatePicker).toBeInTheDocument();
@@ -18,7 +18,7 @@ test('renders the start input field', () => {
1818

1919
test('renders the end input field', () => {
2020
const { getByLabelText } = render(
21-
<Controls data={{ events: [], start: '', end: '' }} handleEventsFiltered={mockFunc} />
21+
<Controls data={{ events: [], start: '', end: '' }} updateMapCenter={mockFunc} handleEventsFiltered={mockFunc} />
2222
);
2323
const endDatePicker = getByLabelText(/until/i);
2424
expect(endDatePicker).toBeInTheDocument();
@@ -27,20 +27,13 @@ test('renders the end input field', () => {
2727

2828
test('renders the day of week checkboxes', () => {
2929
const { getByLabelText } = render(
30-
<Controls data={{ events: [], start: '', end: '' }} handleEventsFiltered={mockFunc} />
30+
<Controls data={{ events: [], start: '', end: '' }} updateMapCenter={mockFunc} handleEventsFiltered={mockFunc} />
3131
);
32-
const sunday = getByLabelText(Day.Sun);
33-
expect(sunday).toBeInTheDocument();
34-
const monday = getByLabelText(Day.Mon);
35-
expect(monday).toBeInTheDocument();
36-
const tuesday = getByLabelText(Day.Tu);
37-
expect(tuesday).toBeInTheDocument();
38-
const wednesday = getByLabelText(Day.Wed);
39-
expect(wednesday).toBeInTheDocument();
40-
const thursday = getByLabelText(Day.Thu);
41-
expect(thursday).toBeInTheDocument();
42-
const friday = getByLabelText(Day.Fri);
43-
expect(friday).toBeInTheDocument();
44-
const saturday = getByLabelText(Day.Sat);
45-
expect(saturday).toBeInTheDocument();
32+
expect(getByLabelText(Day.Sun)).toBeInTheDocument();
33+
expect(getByLabelText(Day.Mon)).toBeInTheDocument();
34+
expect(getByLabelText(Day.Tu)).toBeInTheDocument();
35+
expect(getByLabelText(Day.Wed)).toBeInTheDocument();
36+
expect(getByLabelText(Day.Thu)).toBeInTheDocument();
37+
expect(getByLabelText(Day.Fri)).toBeInTheDocument();
38+
expect(getByLabelText(Day.Sat)).toBeInTheDocument();
4639
});

client/src/components/Controls.tsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, { Component } from 'react';
22
import { FormCheckbox, FormDateField, Toggle } from '.';
3-
import { BikeRide, Day } from '../helpers/format-events';
4-
import { Coordinate } from '../../../types';
3+
import { BikeRide, Coordinate, Day } from '../helpers/format-events';
54
import './Controls.css';
65

76
const allDaysOfWeek = [Day.Sun, Day.Mon, Day.Tu, Day.Wed, Day.Thu, Day.Fri, Day.Sat];
@@ -120,12 +119,17 @@ export class Controls extends Component<ControlsProps, ControlsState> {
120119

121120
handleSelectDay = (e: React.ChangeEvent<any>): void => {
122121
const { daysOfWeek } = this.state;
123-
this.setState({ daysOfWeek: { ...daysOfWeek, [e.target.id]: !!e.target.checked } }, this.applyFilters);
122+
this.setState(
123+
{ daysOfWeek: { ...daysOfWeek, [e.target.id]: !!e.target.checked } },
124+
this.applyFilters
125+
);
124126
};
125127

126128
sortEvents = (): void => {
127129
const { data, handleEventsFiltered } = this.props;
128-
const sorted = data.events.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
130+
const sorted = data.events.sort(
131+
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
132+
);
129133
return handleEventsFiltered(sorted);
130134
};
131135

client/src/components/Event.tsx

+7-7
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function Event(props: EventProps): JSX.Element {
1818
shareable,
1919
cancelled,
2020
title,
21-
geoLookupAddress,
21+
formattedAddress,
2222
newsflash,
2323
venue,
2424
details,
@@ -38,7 +38,7 @@ export function Event(props: EventProps): JSX.Element {
3838
return (
3939
<div onClick={handleListItemClick ? (): void => handleListItemClick(id) : undefined}>
4040
<div className={eventClassName}>
41-
<div className={geoLookupAddress && handleListItemClick ? 'clickable' : ''}>
41+
<div className={formattedAddress && handleListItemClick ? 'clickable' : ''}>
4242
<div className="event-title">
4343
{title} {cancelled && <AlertBanner message="Cancelled" />}
4444
</div>
@@ -67,12 +67,12 @@ export function Event(props: EventProps): JSX.Element {
6767
</div>
6868
<div className="map-detail">
6969
<strong>Displaying location for:</strong>
70-
<div>{geoLookupAddress}</div>
70+
<div>{formattedAddress}</div>
7171
</div>
7272
</div>
7373
{locationEnabled && distanceTo && (
7474
<div className="event-distance-to">
75-
<DistanceTo distanceTo={distanceTo} geoLookupAddress={geoLookupAddress} address={address} />
75+
<DistanceTo distanceTo={distanceTo} formattedAddress={formattedAddress} address={address} />
7676
</div>
7777
)}
7878
</div>
@@ -90,14 +90,14 @@ export function Event(props: EventProps): JSX.Element {
9090

9191
type DistanceToProps = {
9292
distanceTo: number | null;
93-
geoLookupAddress: string | null;
93+
formattedAddress: string | null;
9494
address: string | null;
9595
};
9696
function DistanceTo(props: DistanceToProps): JSX.Element {
97-
const { distanceTo, geoLookupAddress, address } = props;
97+
const { distanceTo, formattedAddress, address } = props;
9898

9999
let message = '';
100-
if (geoLookupAddress && distanceTo) {
100+
if (formattedAddress && distanceTo) {
101101
// geocoding service found address, display distance
102102
message = `${distanceTo} ${distanceTo === 1 ? 'mile' : 'miles'}`;
103103
} else {

client/src/components/Map.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import React, { Component } from 'react';
22
import { LatLngTuple } from 'leaflet';
33
import { Map as LeafletMap, TileLayer, Circle } from 'react-leaflet';
44
import { Marker } from '.';
5-
import { Coordinate } from '../../../types';
6-
import { BikeRide } from '../helpers/format-events';
5+
import { BikeRide, Coordinate } from '../helpers/format-events';
76
import './Map.css';
87

98
const zoom = 13;

client/src/components/Marker.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ export class Marker extends React.Component<MarkerProps, {}> {
2323

2424
render(): JSX.Element {
2525
const { point, locationEnabled } = this.props;
26-
const { latLng, geoLookupAddress, key } = point;
27-
if (!geoLookupAddress) return <div></div>;
26+
const { latLng, formattedAddress, key } = point;
27+
if (!formattedAddress) return <div></div>;
2828
return (
2929
<LeafletMarker ref={this.markerRef} position={[latLng.latitude, latLng.longitude] as LatLngTuple} key={key}>
3030
<Popup>

client/src/helpers/format-events.test.ts

+1-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { transformTime, getTimeForDesc, getDayOfWeek, getISODate, Day } from './format-events';
1+
import { transformTime, getTimeForDesc, getISODate } from './format-events';
22

33
// duplicated tests and algorithm from https://github.com/theholla/shift-ics-generator
44
// TODO: share code using submodule or even better an npm package
@@ -39,18 +39,6 @@ test('getTimeForDesc returns expected duration for PM start, AM end', () => {
3939
expect(getTimeForDesc('23:59:00', '06:00:00')).toEqual('11:59 PM - 6:00 AM');
4040
});
4141

42-
test('getDayOfWeek returns expected day for given ride date', () => {
43-
expect(getDayOfWeek('2020-04-05', '00:00:01')).toEqual(Day.Sun);
44-
expect(getDayOfWeek('2020-04-05')).toEqual(Day.Sun);
45-
expect(getDayOfWeek('2020-04-06')).toEqual(Day.Mon);
46-
expect(getDayOfWeek('2020-04-07')).toEqual(Day.Tu);
47-
expect(getDayOfWeek('2020-04-08')).toEqual(Day.Wed);
48-
expect(getDayOfWeek('2020-04-09')).toEqual(Day.Thu);
49-
expect(getDayOfWeek('2020-04-10')).toEqual(Day.Fri);
50-
expect(getDayOfWeek('2020-04-11')).toEqual(Day.Sat);
51-
expect(getDayOfWeek('2020-03-13', '23:59:59')).toEqual(Day.Fri);
52-
});
53-
5442
test('getISODate returns ISO date with no time', () => {
5543
expect(getISODate(new Date('4/1/2020'))).toEqual('2020-04-01');
5644
});

client/src/helpers/format-events.ts

+39-66
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import { BikeRides4UEvent, RawEventDesc, Coordinate } from '../../../types';
2-
3-
// these date types do not enforce; they just help me recall what the dates (hopefully) look like
4-
type YYYYMMDD = string; // date already in PST in format yyyy-mm-dd
5-
type MDYYYY = string; // date in PST in format m/d/yyyy
6-
type MDYYYYHMMSSSS = string; // date in PST in format m/d/yyyy h:mm:ssss
1+
export type Coordinate = {
2+
readonly latitude: number;
3+
readonly longitude: number;
4+
};
75

86
export enum Day {
97
Sun = 'Sun',
@@ -15,22 +13,41 @@ export enum Day {
1513
Sat = 'Sat',
1614
}
1715

18-
export type FormattedEvent = RawEventDesc & {
16+
type RawEventDesc = {
17+
id: string;
18+
title: string;
19+
details: string;
20+
address: string;
21+
venue: string;
22+
organizer: string;
23+
cancelled: boolean;
24+
date: string; // in format YYYY-MM-DD
25+
audience: string; // enum
26+
shareable: string; // shareable web link
27+
newsflash: string | null;
28+
};
29+
30+
type BikeRides4UEvent = RawEventDesc & {
1931
key: string;
2032
dayOfWeek: Day;
21-
times: string;
2233
latLng: Coordinate;
23-
geoLookupAddress: string;
24-
date: YYYYMMDD;
25-
friendlyDate: MDYYYY;
26-
freshAsOf: MDYYYYHMMSSSS;
34+
formattedAddress: string;
35+
startTime: string;
36+
endTime: string | null;
37+
fetched: number;
38+
};
39+
40+
export type FormattedEvent = BikeRides4UEvent & {
41+
times: string;
42+
friendlyDate: string;
43+
freshAsOf: string;
2744
};
2845

2946
export type BikeRide = FormattedEvent & {
3047
distanceTo?: number;
3148
};
3249

33-
export function getISODate(date: Date, plusMilliseconds?: number): YYYYMMDD {
50+
export function getISODate(date: Date, plusMilliseconds?: number): string {
3451
if (plusMilliseconds) {
3552
return getISODate(new Date(new Date().getTime() + plusMilliseconds));
3653
} else {
@@ -42,8 +59,9 @@ function removeLeadingZero(dateString: string): number {
4259
return Number(dateString);
4360
}
4461

45-
// for now assume all users are also in PST
46-
export function getFriendlyDate(date: YYYYMMDD): MDYYYY {
62+
// for now assume all users want PST
63+
export function getFriendlyDate(date: string): string {
64+
if (!date) return '';
4765
const [year, month, day] = date.split('-');
4866
return `${removeLeadingZero(month)}/${removeLeadingZero(day)}/${year}`;
4967
}
@@ -70,58 +88,13 @@ export function getTimeForDesc(start: string, end: string | null): string {
7088
return time;
7189
}
7290

73-
// TODO: compute on backend to avoid browser-specific date math
74-
export function getDayOfWeek(date: YYYYMMDD, time?: string): Day {
75-
const day = new Date(`${date} ${time || '00:00:00'}`).getDay();
76-
// FIXME: this is a bummer.. can I get from idx instead?
77-
switch (day) {
78-
case 0:
79-
return Day.Sun;
80-
case 1:
81-
return Day.Mon;
82-
case 2:
83-
return Day.Tu;
84-
case 3:
85-
return Day.Wed;
86-
case 4:
87-
return Day.Thu;
88-
case 5:
89-
return Day.Fri;
90-
case 6:
91-
return Day.Sat;
92-
}
93-
console.error(`Cannot parse day of week from date; choosing Sun`, { date });
94-
return Day.Sun;
95-
}
96-
97-
function getEventKey(date: YYYYMMDD, id: string): string {
98-
// repeating events have the same id so we must store them with additional meta
99-
return `${date}-${id}`;
100-
}
101-
10291
export function formatEvents(events: BikeRides4UEvent[]): FormattedEvent[] {
10392
return events
104-
.map(event => {
105-
return {
106-
key: getEventKey(event.date, event.id),
107-
freshAsOf: new Date(event.updated).toLocaleString(),
108-
friendlyDate: getFriendlyDate(event.date),
109-
dayOfWeek: getDayOfWeek(event.date, event.time),
110-
times: getTimeForDesc(event.time, event.endtime),
111-
latLng: event.geoLookup.latLng,
112-
geoLookupAddress: event.geoLookup.formattedAddress,
113-
id: event.id,
114-
title: event.title,
115-
details: event.details,
116-
venue: event.venue,
117-
address: event.address,
118-
audience: event.audience,
119-
organizer: event.organizer,
120-
shareable: event.shareable,
121-
cancelled: event.cancelled,
122-
newsflash: event.newsflash,
123-
date: event.date,
124-
};
125-
})
93+
.map(event => ({
94+
...event,
95+
friendlyDate: getFriendlyDate(event.date),
96+
times: getTimeForDesc(event.startTime, event.endTime),
97+
freshAsOf: new Date(event.fetched).toLocaleString(),
98+
}))
12699
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
127100
}

0 commit comments

Comments
 (0)