Skip to content

Commit 4d559b8

Browse files
authored
2357 display food types (#2516)
* Add food types to Select All endpoint * 2357 add foodTypeFilter to filter panel
1 parent 01e4164 commit 4d559b8

8 files changed

Lines changed: 160 additions & 35 deletions

File tree

client/src/appReducer.jsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ function openTimeFilterReducer(state, action) {
116116
}
117117
}
118118

119+
function foodTypeFilterReducer(state, action) {
120+
switch (action.type) {
121+
case "FOOD_TYPE_FILTER_UPDATED":
122+
return action.foodTypeFilter;
123+
default:
124+
return state;
125+
}
126+
}
127+
119128
function listPanelReducer(state, action) {
120129
switch (action.type) {
121130
case "TOGGLE_LIST_PANEL":
@@ -171,6 +180,7 @@ export function appReducer(state, action) {
171180
filterPanel: filterPanelReducer(state.filterPanel, action),
172181
orgNameFilter: orgNameFilterReducer(state.orgNameFilter, action),
173182
openTimeFilter: openTimeFilterReducer(state.openTimeFilter, action),
183+
foodTypeFilter: foodTypeFilterReducer(state.foodTypeFilter, action),
174184
listPanel: listPanelReducer(state.listPanel, action),
175185
isListPanelVisible: isListPanelVisibleReducer(
176186
state.isListPanelVisible,
@@ -193,6 +203,7 @@ export function getInitialState() {
193203
filterPanel: false,
194204
orgNameFilter: "",
195205
openTimeFilter: { radio: "Show All", day: "", time: "" },
206+
foodTypeFilter: [],
196207
listPanel: true,
197208
isListPanelVisible: false,
198209
position: "0",
@@ -284,6 +295,11 @@ export function useOpenTimeFilter() {
284295
return openTimeFilter;
285296
}
286297

298+
export function useFoodTypeFilter() {
299+
const { foodTypeFilter } = useAppState();
300+
return foodTypeFilter;
301+
}
302+
287303
export function useListPanel() {
288304
const { listPanel } = useAppState();
289305
return listPanel;

client/src/components/FoodSeeker/SearchResults/ResultsFilters/FilterPanel.jsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ import {
2929
useFilterPanel,
3030
useOrgNameFilter,
3131
useOpenTimeFilter,
32+
useFoodTypeFilter,
3233
} from "../../../../appReducer";
3334
import { Clear } from "@mui/icons-material";
35+
import { foodTypeLabelObject } from "helpers/Constants";
3436

3537
const checkedStyle = {
3638
"&.Mui-checked": {
@@ -51,6 +53,7 @@ export default function FilterPanel({ mealPantry }) {
5153
const open = useFilterPanel();
5254
const openTime = useOpenTimeFilter();
5355
const orgNameFilter = useOrgNameFilter();
56+
const foodTypeFilter = useFoodTypeFilter();
5457

5558
const handleRadioChange = (event) => {
5659
const name = event.target.name;
@@ -271,6 +274,42 @@ export default function FilterPanel({ mealPantry }) {
271274
/>
272275
</RadioGroup>
273276
<Divider sx={{ mt: 2 }} />
277+
<Typography variant="h4" sx={yPadding}>
278+
Food Types
279+
</Typography>
280+
<List>
281+
{Object.keys(foodTypeLabelObject).map((foodType) => (
282+
<ListItem key={foodType} sx={{ padding: 0 }}>
283+
<ListItemButton sx={{ padding: 0 }}>
284+
<FormControlLabel
285+
sx={{ width: "100%" }}
286+
key={foodType}
287+
control={
288+
<Checkbox
289+
sx={checkedStyle}
290+
checked={foodTypeFilter.includes(foodType)}
291+
onChange={(e) => {
292+
const newFoodTypeFilter = e.target.checked
293+
? [...foodTypeFilter, foodType]
294+
: foodTypeFilter.filter((type) => type !== foodType);
295+
296+
dispatch({
297+
type: "FOOD_TYPE_FILTER_UPDATED",
298+
foodTypeFilter: newFoodTypeFilter,
299+
});
300+
}}
301+
/>
302+
}
303+
label={
304+
<Stack direction="row" alignItems="center">
305+
<ListItemText primary={foodTypeLabelObject[foodType]} />
306+
</Stack>
307+
}
308+
/>
309+
</ListItemButton>
310+
</ListItem>
311+
))}
312+
</List>
274313
</Box>
275314
</Drawer>
276315
);

client/src/components/FoodSeeker/SearchResults/StakeholderDetails/StakeholderDetails.jsx

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import ForkIcon from "icons/ForkIcon";
3131
import AppleIcon from "icons/AppleIcon";
3232
import IosShareIcon from "@mui/icons-material/IosShare";
3333
import SubdirectoryArrowRightIcon from "@mui/icons-material/SubdirectoryArrowRight";
34-
import { useEffect, useState } from "react";
34+
import { useEffect, useMemo, useState } from "react";
3535
import { useNavigate } from "react-router-dom";
3636
import * as analytics from "services/analytics";
3737
import {
@@ -55,6 +55,7 @@ import XIcon from "@mui/icons-material/X";
5555
import PinterestIcon from "@mui/icons-material/Pinterest";
5656
import IconButton from "@mui/material/IconButton";
5757
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
58+
import { foodTypeLabelObject } from "helpers/Constants";
5859

5960
const MinorHeading = styled(Typography)(({ theme }) => ({
6061
variant: "h5",
@@ -97,6 +98,20 @@ const StakeholderDetails = ({ onBackClick, isDesktop }) => {
9798
const position = usePosition();
9899
const [paddingBottom, setPaddingBottom] = useState(30);
99100

101+
const foodTypes = useMemo(() => {
102+
return (
103+
Object.keys(foodTypeLabelObject)
104+
.filter((foodType) => selectedOrganization[foodType])
105+
.map((foodType) => {
106+
return foodTypeLabelObject[foodType];
107+
})
108+
.join(", ") +
109+
(selectedOrganization.foodTypes
110+
? `, ${selectedOrganization.foodTypes}`
111+
: "")
112+
);
113+
}, [selectedOrganization]);
114+
100115
useEffect(() => {
101116
const windowHeight = window.innerHeight / 100;
102117
if (
@@ -529,23 +544,6 @@ const StakeholderDetails = ({ onBackClick, isDesktop }) => {
529544
</Stack>
530545
)}
531546

532-
{selectedOrganization.foodTypes && (
533-
<Box textAlign="left">
534-
<Typography
535-
variant="body2"
536-
component="p"
537-
key={selectedOrganization.id}
538-
sx={{
539-
alignSelf: "flex-start",
540-
margin: "1em 0.25em 0.5em 0",
541-
fontSize: "1rem !important",
542-
}}
543-
>
544-
{selectedOrganization.foodTypes}
545-
</Typography>
546-
</Box>
547-
)}
548-
549547
<Stack
550548
direction="row"
551549
justifyContent="start"
@@ -682,10 +680,15 @@ const StakeholderDetails = ({ onBackClick, isDesktop }) => {
682680
</>
683681
)}
684682

685-
{selectedOrganization.items && (
683+
{(selectedOrganization.items || foodTypes.length > 0) && (
686684
<>
687685
<MinorHeading>Items Available</MinorHeading>
688-
<DetailText>{selectedOrganization.items}</DetailText>
686+
{selectedOrganization.items && (
687+
<DetailText>{selectedOrganization.items}</DetailText>
688+
)}
689+
{foodTypes.length > 0 && (
690+
<DetailText>{foodTypes}</DetailText>
691+
)}
689692
</>
690693
)}
691694
{hasAnySocialMediaUrl(selectedOrganization) && (

client/src/helpers/Constants.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ const TENANT_MAINTAINER_LOGO_URLS = {
9292
6: hackForLaLogoUrl,
9393
};
9494

95-
9695
export const DEFAULT_VIEWPORTS = {
9796
1: {
9897
center: { latitude: 34.0354899, longitude: -118.2439235 },
@@ -183,13 +182,18 @@ export const TENANT_METADATA = {
183182
tenantLogoUrl: TENANT_LOGO_URL,
184183
};
185184

186-
export const LINKEDIN_REGEX =
187-
/^https?:\/\/(www\.)?linkedin\.com(\/.*)?$/;
188-
export const FACEBOOK_REGEX =
189-
/^https?:\/\/(www\.)?facebook\.com(\/.*)?$/;
190-
export const INSTAGRAM_REGEX =
191-
/^https?:\/\/(www\.)?instagram\.com(\/.*)?$/;
192-
export const PINTEREST_REGEX =
193-
/^https?:\/\/(www\.)?pinterest\.com(\/.*)?$/;
185+
export const LINKEDIN_REGEX = /^https?:\/\/(www\.)?linkedin\.com(\/.*)?$/;
186+
export const FACEBOOK_REGEX = /^https?:\/\/(www\.)?facebook\.com(\/.*)?$/;
187+
export const INSTAGRAM_REGEX = /^https?:\/\/(www\.)?instagram\.com(\/.*)?$/;
188+
export const PINTEREST_REGEX = /^https?:\/\/(www\.)?pinterest\.com(\/.*)?$/;
194189
export const TWITTER_REGEX =
195190
/^https?:\/\/(www\.)?(twitter\.com|x\.com)(\/.*)?$/;
191+
192+
export const foodTypeLabelObject = {
193+
foodBakery: "Baked Goods",
194+
foodDairy: "Dairy",
195+
foodDryGoods: "Dry Goods",
196+
foodMeat: "Meat",
197+
foodPrepared: "Prepared Food",
198+
foodProduce: "Produce",
199+
};

client/src/hooks/useOrganizationBests.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useCallback, useState } from "react";
55
import {
66
DEFAULT_COORDINATES,
77
useAppDispatch,
8+
useFoodTypeFilter,
89
useOpenTimeFilter,
910
useOrgNameFilter,
1011
useSearchCoordinates,
@@ -40,6 +41,7 @@ export default function useOrganizationBests() {
4041
const searchCoordinates = useSearchCoordinates();
4142
const openTimeFilter = useOpenTimeFilter();
4243
const orgNameFilter = useOrgNameFilter();
44+
const foodTypeFilter = useFoodTypeFilter();
4345
const { tenantTimeZone } = useSiteContext();
4446

4547
const longitude =
@@ -96,6 +98,13 @@ export default function useOrganizationBests() {
9698
.every((word) => stakeholder.name.toLowerCase().includes(word));
9799
});
98100
}
101+
if (filters.foodTypeFilter) {
102+
filteredStakeholders = filteredStakeholders.filter((stakeholder) => {
103+
return filters.foodTypeFilter.every((foodType) => {
104+
return stakeholder[foodType] === true;
105+
});
106+
});
107+
}
99108

100109
const stakeholdersWithDistances = computeDistances(
101110
latitude,
@@ -145,6 +154,9 @@ export default function useOrganizationBests() {
145154
if (orgNameFilter) {
146155
filters.orgNameFilter = orgNameFilter;
147156
}
157+
if (foodTypeFilter.length) {
158+
filters.foodTypeFilter = foodTypeFilter;
159+
}
148160

149161
let stakeholders;
150162
const isStaleData = checkIfStaleData();
@@ -166,7 +178,14 @@ export default function useOrganizationBests() {
166178
return Promise.reject(err);
167179
}
168180
},
169-
[openTimeFilter, latitude, longitude, processStakeholders, orgNameFilter]
181+
[
182+
openTimeFilter,
183+
latitude,
184+
longitude,
185+
processStakeholders,
186+
orgNameFilter,
187+
foodTypeFilter,
188+
]
170189
);
171190

172191
const getById = useCallback(async (id) => {

client/tests/helpers/mocks.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ function makeStakeholdersResponse() {
167167
neighborhoodId: null,
168168
neighborhoodName: null,
169169
isVerified: false,
170+
foodBakery: false,
171+
foodDryGoods: false,
172+
foodProduce: false,
173+
foodDairy: false,
174+
foodPrepared: false,
175+
foodMeat: false,
170176
parentOrganizationId: null,
171177
allowWalkins: true,
172178
hoursNotes: "",
@@ -231,6 +237,12 @@ function makeStakeholdersResponse() {
231237
neighborhoodId: null,
232238
neighborhoodName: null,
233239
isVerified: false,
240+
foodBakery: false,
241+
foodDryGoods: true,
242+
foodProduce: false,
243+
foodDairy: true,
244+
foodPrepared: false,
245+
foodMeat: false,
234246
parentOrganizationId: null,
235247
allowWalkins: false,
236248
hoursNotes: "",
@@ -288,6 +300,12 @@ function makeStakeholdersResponse() {
288300
neighborhoodId: null,
289301
neighborhoodName: null,
290302
isVerified: false,
303+
foodBakery: false,
304+
foodDryGoods: true,
305+
foodProduce: false,
306+
foodDairy: false,
307+
foodPrepared: false,
308+
foodMeat: false,
291309
parentOrganizationId: null,
292310
allowWalkins: false,
293311
hoursNotes: "",

client/tests/organizations.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,28 @@ test.describe("Organizations", () => {
8484
await expect(page.getByText("Stakeholder 2")).toBeVisible();
8585
await expect(page.getByText("Stakeholder 3")).toBeHidden();
8686
});
87+
test("clicking Dry Goods filter should show Stakeholders 2 and 3 and not show Stakeholder 1", async ({
88+
page,
89+
}) => {
90+
await mockRequests(page);
91+
await page.goto("/organizations");
92+
await page.getByRole("button", { name: "More Filters" }).click();
93+
await page.getByRole("button", { name: "Dry Goods" }).click();
94+
await expect(page.getByText("Stakeholder 1")).toBeHidden();
95+
await expect(page.getByText("Stakeholder 2")).toBeVisible();
96+
await expect(page.getByText("Stakeholder 3")).toBeVisible();
97+
});
98+
99+
test("clicking Dry Goods and Dairy filter should show Stakeholders 2 and not show Stakeholder 1 and 3", async ({
100+
page,
101+
}) => {
102+
await mockRequests(page);
103+
await page.goto("/organizations");
104+
await page.getByRole("button", { name: "More Filters" }).click();
105+
await page.getByRole("button", { name: "Dry Goods" }).click();
106+
await page.getByRole("button", { name: "Dairy" }).click();
107+
await expect(page.getByText("Stakeholder 1")).toBeHidden();
108+
await expect(page.getByText("Stakeholder 2")).toBeVisible();
109+
await expect(page.getByText("Stakeholder 3")).toBeHidden();
110+
});
87111
});

server/app/services/stakeholder-best-service.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ const booleanEitherClause = (columnName: string, value?: string) => {
2929
return value === "true"
3030
? ` and ${columnName} is true `
3131
: value === "false"
32-
? ` and ${columnName} is false `
33-
: "";
32+
? ` and ${columnName} is false `
33+
: "";
3434
};
3535

3636
const selectAll = async ({ tenantId }: { tenantId: string }) => {
@@ -50,10 +50,12 @@ const selectAll = async ({ tenantId }: { tenantId: string }) => {
5050
s.twitter, s.pinterest, s.linkedin, s.description,
5151
s.review_notes, s.instagram, s.admin_contact_name,
5252
s.admin_contact_phone, s.admin_contact_email,
53-
s.covid_notes, s.food_types, s.languages,
53+
s.covid_notes, s.food_types, s.v_food_types, s.languages,
5454
s.verification_status_id, s.inactive_temporary,
5555
array_to_json(s.hours) as hours, s.category_ids,
5656
s.neighborhood_id, n.name as neighborhood_name, s.is_verified,
57+
s.food_bakery, s.food_dry_goods, s.food_produce,
58+
s.food_dairy, s.food_prepared, s.food_meat,
5759
s.parent_organization_id,
5860
s.allow_walkins, s.hours_notes, s.tags
5961
FROM stakeholder_best s
@@ -161,8 +163,8 @@ const search = async ({
161163
maxLat && maxLng && minLat && minLng
162164
? buildBounds({ maxLat, maxLng, minLat, minLng })
163165
: Number(distance) && locationClause
164-
? `AND ${locationClause} < ${distance}`
165-
: ""
166+
? `AND ${locationClause} < ${distance}`
167+
: ""
166168
}
167169
${booleanEitherClause("s.inactive", isInactive)}
168170
${

0 commit comments

Comments
 (0)