Skip to content

Commit c4e5193

Browse files
committed
Ash Wednesday filter
1 parent a3db7c7 commit c4e5193

File tree

15 files changed

+180
-80
lines changed

15 files changed

+180
-80
lines changed

src/App.jsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { uiActions } from "@/store/slices/uiSlice";
1919
import { filtersActions } from "@/store/slices/filtersSlice";
2020
import { selectionActions } from "@/store/slices/selectionSlice";
2121

22-
import { computeGoodFriday } from "@/domain/dateUtils";
2322
import { normalizeFeatureCollection, matchesSearch } from "@/domain/featureUtils";
2423
import { featureIsInBounds, filterFeatures } from "@/domain/filterUtils";
2524
import { logClientError } from "@/utils/errorLogging";
@@ -37,8 +36,6 @@ const App = () => {
3736
const { data, isLoading, isFetching, error } = useGetFishfriesQuery();
3837
const dataSource = data?.__source || "primary";
3938

40-
const goodFridayDate = useMemo(() => computeGoodFriday(moment().year()), []);
41-
4239
const allFeatures = useMemo(() => {
4340
return normalizeFeatureCollection(data).sort((left, right) => {
4441
return (left.properties.venue_name || "").localeCompare(right.properties.venue_name || "");
@@ -54,8 +51,8 @@ const App = () => {
5451
}, [allFeatures]);
5552

5653
const filteredFeatures = useMemo(() => {
57-
return filterFeatures(allFeatures, filters, goodFridayDate);
58-
}, [allFeatures, filters, goodFridayDate]);
54+
return filterFeatures(allFeatures, filters);
55+
}, [allFeatures, filters]);
5956

6057
const visibleFeatures = useMemo(() => {
6158
if (!mapState.overlayVisible) {
@@ -176,7 +173,6 @@ const App = () => {
176173
dispatch(selectionActions.setHighlightedFeatureId(null));
177174
}}
178175
feature={selectedFeature}
179-
goodFridayDate={goodFridayDate}
180176
currentYear={moment().year()}
181177
/>
182178
</ModalErrorBoundary>

src/domain/dateUtils.js

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,47 @@ export function computeGoodFriday(year) {
2626
return moment(easter(year)).subtract(2, "d");
2727
}
2828

29-
export function isOpenOnGoodFriday(events, goodFridayDate) {
29+
export function computeAshWednesday(year) {
30+
return moment(easter(year)).subtract(46, "d");
31+
}
32+
33+
export function deriveLiturgicalOpenFlags(events) {
3034
if (!Array.isArray(events)) {
31-
return false;
35+
return {
36+
GoodFriday: false,
37+
AshWednesday: false
38+
};
3239
}
3340

34-
return events.some((event) => moment(event?.dt_start).isSame(goodFridayDate, "day"));
41+
let openGoodFriday = false;
42+
let openAshWednesday = false;
43+
44+
events.forEach((event) => {
45+
const start = moment(event?.dt_start);
46+
if (!start.isValid()) {
47+
return;
48+
}
49+
50+
const eventYear = start.year();
51+
const goodFridayDate = computeGoodFriday(eventYear);
52+
const ashWednesdayDate = computeAshWednesday(eventYear);
53+
54+
if (start.isSame(goodFridayDate, "day")) {
55+
openGoodFriday = true;
56+
}
57+
58+
if (start.isSame(ashWednesdayDate, "day")) {
59+
openAshWednesday = true;
60+
}
61+
});
62+
63+
return {
64+
GoodFriday: openGoodFriday,
65+
AshWednesday: openAshWednesday
66+
};
3567
}
3668

37-
export function parseDateTimes(events, nowMoment, goodFridayDate) {
69+
export function parseDateTimes(events, nowMoment) {
3870
const now = nowMoment || moment();
3971
const sortList = [];
4072
const eventList = Array.isArray(events) ? events : [];
@@ -66,8 +98,10 @@ export function parseDateTimes(events, nowMoment, goodFridayDate) {
6698
sortList.forEach((pair) => {
6799
const start = pair[0];
68100
const end = pair[1];
101+
const goodFridayDate = computeGoodFriday(start.year());
102+
const isGoodFridayEvent = start.isSame(goodFridayDate, "day");
69103

70-
if (moment(start).isSame(goodFridayDate, "day")) {
104+
if (isGoodFridayEvent) {
71105
openGoodFriday = true;
72106
}
73107

@@ -77,7 +111,7 @@ export function parseDateTimes(events, nowMoment, goodFridayDate) {
77111
}
78112

79113
let label;
80-
if (openGoodFriday) {
114+
if (isGoodFridayEvent) {
81115
label = `Open Good ${start.format("dddd, MMMM Do")}, ${start.format("h:mm a")} to ${end.format(
82116
"h:mm a"
83117
)}`;

src/domain/featureUtils.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { deriveLiturgicalOpenFlags } from "@/domain/dateUtils";
2+
13
const iconPath = "/assets/img/";
24

35
export const iconLookup = {
@@ -19,6 +21,7 @@ export const FILTER_KEYS = [
1921
"take_out",
2022
"handicap",
2123
"GoodFriday",
24+
"AshWednesday",
2225
"publish"
2326
];
2427

@@ -51,6 +54,7 @@ export function resolveIcon(feature) {
5154

5255
export function normalizeFeature(rawFeature, index = 0) {
5356
const properties = rawFeature?.properties || {};
57+
const liturgicalFlags = deriveLiturgicalOpenFlags(properties.events);
5458
const geometry = rawFeature?.geometry || {};
5559
const coordinates = geometry?.coordinates || [];
5660

@@ -59,6 +63,8 @@ export function normalizeFeature(rawFeature, index = 0) {
5963
id: `${rawFeature?.id ?? index}`,
6064
properties: {
6165
...properties,
66+
GoodFriday: liturgicalFlags.GoodFriday,
67+
AshWednesday: liturgicalFlags.AshWednesday,
6268
website: normalizeUrl(properties.website),
6369
menu: {
6470
text: properties?.menu?.text || "",

src/domain/filterUtils.js

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { isOpenOnGoodFriday } from "@/domain/dateUtils";
2-
3-
export function filterFeature(feature, filters, goodFridayDate) {
1+
export function filterFeature(feature, filters) {
42
let noFiltersApplied = true;
53
const results = [];
64

@@ -13,12 +11,7 @@ export function filterFeature(feature, filters, goodFridayDate) {
1311
return;
1412
}
1513

16-
let propValue = false;
17-
if (key === "GoodFriday") {
18-
propValue = isOpenOnGoodFriday(feature?.properties?.events, goodFridayDate);
19-
} else {
20-
propValue = feature?.properties?.[key];
21-
}
14+
const propValue = feature?.properties?.[key];
2215

2316
results.push(checked === propValue);
2417
});
@@ -30,8 +23,8 @@ export function filterFeature(feature, filters, goodFridayDate) {
3023
return !results.includes(false);
3124
}
3225

33-
export function filterFeatures(features, filters, goodFridayDate) {
34-
return features.filter((feature) => filterFeature(feature, filters, goodFridayDate));
26+
export function filterFeatures(features, filters) {
27+
return features.filter((feature) => filterFeature(feature, filters));
3528
}
3629

3730
export function hasActiveFilters(filters) {

src/features/filters/FilterModal.jsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
33
import { Button, Col, Form, ListGroup, Modal, Row } from "react-bootstrap";
4-
import { faBagShopping, faBeerMugEmpty, faClock, faUtensils, faWheelchair } from "@/icons/fontAwesome";
4+
import { faBagShopping, faBeerMugEmpty, faClock, faUtensils, faWheelchair, faPlus } from "@/icons/fontAwesome";
55
import "./FilterModal.css";
66

77
const FILTER_FIELDS = [
@@ -40,14 +40,22 @@ const FILTER_FIELDS = [
4040
</>
4141
)
4242
},
43+
{
44+
key: "AshWednesday",
45+
label: (
46+
<>
47+
Open Ash Wednesday <FontAwesomeIcon icon={faPlus} aria-hidden="true" />
48+
</>
49+
)
50+
},
4351
{
4452
key: "GoodFriday",
4553
label: (
4654
<>
47-
Open Good Friday <FontAwesomeIcon icon={faBagShopping} aria-hidden="true" />
55+
Open Good Friday <FontAwesomeIcon icon={faPlus} aria-hidden="true" />
4856
</>
4957
)
50-
}
58+
},
5159
];
5260

5361
const FilterModal = ({ show, onHide, filters, onChange }) => {
@@ -87,7 +95,7 @@ const FilterModal = ({ show, onHide, filters, onChange }) => {
8795
</ListGroup>
8896
</Col>
8997
</Row>
90-
98+
9199
<Row>
92100
<Col>
93101
<ListGroup className="publish-filter-list my-2">
@@ -112,9 +120,9 @@ const FilterModal = ({ show, onHide, filters, onChange }) => {
112120
<Row>
113121
<Col >
114122
<p>
115-
Note that while we try our best to verify all details about Fish Fries, some details may be
116-
incomplete. Please check with the Fish Fry venue ahead of time and rely on the above filters at your
117-
own discretion.
123+
Note that while we try our best to verify all details about Fish Fries, some details may be
124+
incomplete. Please check with the Fish Fry venue ahead of time and rely on the above filters at your
125+
own discretion.
118126
</p>
119127
</Col>
120128
</Row>

src/features/layout/Sidebar.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const Sidebar = ({ features }) => {
4141
<Card id="features" className="h-100 border-0 rounded-0">
4242
<Card.Header>
4343
<h3 className="h5 mt-3 d-flex align-items-center justify-content-between">
44-
<span>Fish Fry Filter</span>
44+
<span>Fish Fry Venues <span className="small text-muted">({features.length})</span></span>
4545
<Button
4646
type="button"
4747
variant="primary"

src/features/modals/FeatureModal.jsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function BooleanPanel({ label, value }) {
4343
);
4444
}
4545

46-
const FeatureModal = ({ show, onHide, feature, goodFridayDate, currentYear }) => {
46+
const FeatureModal = ({ show, onHide, feature, currentYear }) => {
4747
const events = useMemo(() => {
4848
if (!feature) {
4949
return {
@@ -53,8 +53,8 @@ const FeatureModal = ({ show, onHide, feature, goodFridayDate, currentYear }) =>
5353
};
5454
}
5555

56-
return parseDateTimes(feature.properties.events, moment(), goodFridayDate);
57-
}, [feature, goodFridayDate]);
56+
return parseDateTimes(feature.properties.events, moment());
57+
}, [feature]);
5858

5959
if (!feature) {
6060
return null;
@@ -66,7 +66,8 @@ const FeatureModal = ({ show, onHide, feature, goodFridayDate, currentYear }) =>
6666
{ label: "Homemade Pierogies", value: feature.properties.homemade_pierogies },
6767
{ label: "Alcohol Served", value: feature.properties.alcohol },
6868
{ label: "Lunch Served", value: feature.properties.lunch },
69-
{ label: "Open Good Friday", value: events.GoodFriday },
69+
{ label: "Open Good Friday", value: feature.properties.GoodFriday },
70+
{ label: "Open Ash Wednesday", value: feature.properties.AshWednesday },
7071
{ label: "Drive-Thru Available", value: feature.properties.drive_thru },
7172
{ label: "Take-Out Available", value: feature.properties.take_out },
7273
{ label: "Handicap Accessible", value: feature.properties.handicap }

tests/parity/filters.spec.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const { waitForSidebarData, openFilterModal } = require("./helpers");
44
test("filter state toggles and persists in sidebar CTA", async ({ page }) => {
55
await page.goto("/");
66
await waitForSidebarData(page);
7-
const initialRows = await page.locator("#feature-list tbody tr").count();
7+
const initialRows = await page.locator("#feature-list .feature-row").count();
88

99
await openFilterModal(page);
1010
await expect(page.getByText("Find those Fish Fries!")).toBeVisible();
@@ -16,13 +16,13 @@ test("filter state toggles and persists in sidebar CTA", async ({ page }) => {
1616
await expect(page.locator("#filterSidebar-btn")).toHaveClass(/btn-primary/);
1717
await expect
1818
.poll(async () => {
19-
return page.locator("#feature-list tbody tr").count();
19+
return page.locator("#feature-list .feature-row").count();
2020
})
2121
.toBeLessThanOrEqual(initialRows);
2222

23-
const filteredRows = await page.locator("#feature-list tbody tr").count();
23+
const filteredRows = await page.locator("#feature-list .feature-row").count();
2424
if (filteredRows > 0) {
25-
await page.locator("#feature-list tbody tr").first().click();
25+
await page.locator("#feature-list .feature-row").first().click();
2626
await expect(page.locator("#feature-title")).toBeVisible({ timeout: 10000 });
2727
await expect(page.getByText("has not yet been verified this year")).toHaveCount(0);
2828
await page.locator(".modal.show").getByRole("button", { name: "Close" }).last().click();
@@ -38,7 +38,7 @@ test("filter state toggles and persists in sidebar CTA", async ({ page }) => {
3838
test("good friday filter can be toggled without breaking results", async ({ page }) => {
3939
await page.goto("/");
4040
await waitForSidebarData(page);
41-
const before = await page.locator("#feature-list tbody tr").count();
41+
const before = await page.locator("#feature-list .feature-row").count();
4242

4343
await openFilterModal(page);
4444
await page.locator("input#GoodFriday").check();
@@ -47,7 +47,7 @@ test("good friday filter can be toggled without breaking results", async ({ page
4747
await expect(page.locator("#filterSidebar-btn")).toContainText("Filtered");
4848
await expect
4949
.poll(async () => {
50-
return page.locator("#feature-list tbody tr").count();
50+
return page.locator("#feature-list .feature-row").count();
5151
})
5252
.toBeLessThanOrEqual(before);
5353

@@ -56,3 +56,25 @@ test("good friday filter can be toggled without breaking results", async ({ page
5656
await page.getByRole("button", { name: "Find those Fish Fries!" }).click();
5757
await expect(page.locator("#filterSidebar-btn")).toContainText("Filter");
5858
});
59+
60+
test("ash wednesday filter can be toggled without breaking results", async ({ page }) => {
61+
await page.goto("/");
62+
await waitForSidebarData(page);
63+
const before = await page.locator("#feature-list .feature-row").count();
64+
65+
await openFilterModal(page);
66+
await page.locator("input#AshWednesday").check();
67+
await page.getByRole("button", { name: "Find those Fish Fries!" }).click();
68+
69+
await expect(page.locator("#filterSidebar-btn")).toContainText("Filtered");
70+
await expect
71+
.poll(async () => {
72+
return page.locator("#feature-list .feature-row").count();
73+
})
74+
.toBeLessThanOrEqual(before);
75+
76+
await page.locator("#filterSidebar-btn").click();
77+
await page.locator("input#AshWednesday").uncheck();
78+
await page.getByRole("button", { name: "Find those Fish Fries!" }).click();
79+
await expect(page.locator("#filterSidebar-btn")).toContainText("Filter");
80+
});

tests/parity/helpers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ async function waitForSidebarData(page, minRows = 1) {
44
await expect(page.locator("#map")).toBeVisible();
55
await expect
66
.poll(async () => {
7-
return page.locator("#feature-list tbody tr").count();
7+
return page.locator("#feature-list .feature-row").count();
88
}, { timeout: 30000 })
99
.toBeGreaterThanOrEqual(minRows);
1010
}

tests/parity/map.spec.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ test("overlay toggle hides and restores sidebar rows", async ({ page }) => {
3333
await expect(overlayToggle).not.toBeChecked();
3434
await expect
3535
.poll(async () => {
36-
return page.locator("#feature-list tbody tr").count();
36+
return page.locator("#feature-list .feature-row").count();
3737
})
3838
.toBe(0);
3939

@@ -63,13 +63,13 @@ test("sidebar rows resync after map moveend from feature focus", async ({ page }
6363
await page.goto("/");
6464
await waitForSidebarData(page, 2);
6565

66-
const initialRows = await page.locator("#feature-list tbody tr").count();
67-
await page.locator("#feature-list tbody tr").first().click();
66+
const initialRows = await page.locator("#feature-list .feature-row").count();
67+
await page.locator("#feature-list .feature-row").first().click();
6868
await expect(page.locator("#feature-title")).toBeVisible({ timeout: 10000 });
6969

7070
await expect
7171
.poll(async () => {
72-
return page.locator("#feature-list tbody tr").count();
72+
return page.locator("#feature-list .feature-row").count();
7373
})
7474
.not.toBe(initialRows);
7575
});

0 commit comments

Comments
 (0)