From 1c6aa13337fca3b1b850c19a9bb289059e9bf18e Mon Sep 17 00:00:00 2001 From: Owen Yamauchi Date: Fri, 27 Sep 2024 20:29:42 -0400 Subject: [PATCH] Refactor logic for HEAR multi-item incentive names (#188) ## Description This is what I was talking about in #187. It ended up being more involved than I thought, because Jest couldn't handle the JSX in the file `projects.tsx`. I tried other values of the `jsx` setting in tsconfig.json, but then Jest couldn't handle the magical `jsx:...` imports in projects.tsx (understandable; that's done using Parcel magic). So I factored out the icons from projects.tsx into a new file, and now the unit test works fine. ## Test Plan `yarn test`. The unit test is unmodified but still passes. Look at RI's HEAR incentive for stoves/dryers, and make sure it shows up and has the correct headline. Make sure the icons in the projects dropdown are correct. The one in the dropdown button should be purple, and in the menu should be grey-700 (i.e. almost black). --- src/item-name.ts | 63 ++++++++----------------------- src/project-icons.tsx | 24 ++++++++++++ src/{projects.tsx => projects.ts} | 21 ----------- src/state-incentive-details.tsx | 3 +- static/icons/lawnmower.svg | 2 +- 5 files changed, 43 insertions(+), 70 deletions(-) create mode 100644 src/project-icons.tsx rename src/{projects.tsx => projects.ts} (67%) diff --git a/src/item-name.ts b/src/item-name.ts index 428f0ac..09c69f1 100644 --- a/src/item-name.ts +++ b/src/item-name.ts @@ -1,6 +1,6 @@ import { ItemType } from './api/calculator-types-v1'; import { MsgFn } from './i18n/use-translated'; -import { Project } from './projects'; +import { Project, PROJECTS } from './projects'; type ItemGroup = | 'air_source_heat_pump' @@ -15,8 +15,7 @@ type ItemGroup = | 'weatherization' | 'audit_and_weatherization' | 'water_heater' - | 'electric_outdoor_equipment' - | 'hear_projects'; + | 'electric_outdoor_equipment'; const ALL_INSULATION: ItemType[] = [ 'attic_or_roof_insulation', @@ -116,17 +115,13 @@ const ITEM_GROUPS: { group: ItemGroup; members: Set }[] = [ group: 'water_heater', members: new Set(['heat_pump_water_heater', 'non_heat_pump_water_heater']), }, - { - group: 'hear_projects', - members: new Set(['heat_pump_clothes_dryer', 'electric_stove']), - }, ]; const itemsBelongToGroup = (items: ItemType[], members: Set) => { return items.every(i => members.has(i)); }; -const multipleItemsName = (items: ItemType[], msg: MsgFn, project: Project) => { +const multipleItemsName = (items: ItemType[], msg: MsgFn) => { // For a multiple-items case, check whether all the items are in one of the // defined groups. for (const { group, members } of ITEM_GROUPS) { @@ -180,8 +175,6 @@ const multipleItemsName = (items: ItemType[], msg: MsgFn, project: Project) => { return msg('electric outdoor equipment', { desc: 'e.g. "$100 off [this string]"', }); - case 'hear_projects': - return hearName(items, msg, project); default: { // This will be a type error if the above switch is not exhaustive const unknownGroup: never = group; @@ -194,51 +187,27 @@ const multipleItemsName = (items: ItemType[], msg: MsgFn, project: Project) => { return null; }; -const hearName = (items: ItemType[], msg: MsgFn, project: Project) => { - const HEAR_INCENTIVE_PROJECT_MSG_LIST: { - item: ItemType; - project: Project; - msg: string; - }[] = [ - { - item: 'heat_pump_clothes_dryer', - project: 'clothes_dryer', - msg: msg('a heat pump clothes dryer', { - desc: 'e.g. "$100 off [this string]"', - }), - }, - - { - item: 'electric_stove', - project: 'cooking', - msg: msg('an electric/induction stove', { - desc: 'e.g. "$100 off [this string]"', - }), - }, - ]; - - const match = HEAR_INCENTIVE_PROJECT_MSG_LIST.find( - group => items.includes(group.item) && project === group.project, - ); - - if (!match) return null; - - return match.msg; -}; - /** * TODO this is an internationalization sin. Figure out something better! */ -export const itemName = (items: ItemType[], msg: MsgFn, project: Project) => { - if (items.length > 1) { - return multipleItemsName(items, msg, project); +export const itemName = ( + incentiveItems: ItemType[], + msg: MsgFn, + project: Project, +) => { + const itemsToRender = incentiveItems.filter(item => + PROJECTS[project].items.includes(item), + ); + + if (itemsToRender.length > 1) { + return multipleItemsName(itemsToRender, msg); } - if (items.length !== 1) { + if (itemsToRender.length !== 1) { return null; } - const item = items[0]; + const item = itemsToRender[0]; switch (item) { case 'air_sealing': return msg('air sealing', { desc: 'e.g. "$100 off [this string]"' }); diff --git a/src/project-icons.tsx b/src/project-icons.tsx new file mode 100644 index 0000000..591e06a --- /dev/null +++ b/src/project-icons.tsx @@ -0,0 +1,24 @@ +import BatteryIcon from 'jsx:../static/icons/battery.svg'; +import ClothesDryerIcon from 'jsx:../static/icons/clothes-dryer.svg'; +import CookingIcon from 'jsx:../static/icons/cooking.svg'; +import ElectricalWiringIcon from 'jsx:../static/icons/electrical-wiring.svg'; +import EvIcon from 'jsx:../static/icons/ev.svg'; +import HvacIcon from 'jsx:../static/icons/hvac.svg'; +import LawnMowerIcon from 'jsx:../static/icons/lawnmower.svg'; +import SolarIcon from 'jsx:../static/icons/solar.svg'; +import WaterHeaterIcon from 'jsx:../static/icons/water-heater.svg'; +import WeatherizationIcon from 'jsx:../static/icons/weatherization.svg'; +import { Project } from './projects'; + +export const PROJECT_ICONS: { [p in Project]: () => React.ReactElement } = { + clothes_dryer: () => , + hvac: () => , + ev: () => , + solar: () => , + battery: () => , + water_heater: () => , + cooking: () => , + wiring: () => , + weatherization_and_efficiency: () => , + lawn_care: () => , +}; diff --git a/src/projects.tsx b/src/projects.ts similarity index 67% rename from src/projects.tsx rename to src/projects.ts index 3741499..a43624d 100644 --- a/src/projects.tsx +++ b/src/projects.ts @@ -1,19 +1,8 @@ -import BatteryIcon from 'jsx:../static/icons/battery.svg'; -import ClothesDryerIcon from 'jsx:../static/icons/clothes-dryer.svg'; -import CookingIcon from 'jsx:../static/icons/cooking.svg'; -import ElectricalWiringIcon from 'jsx:../static/icons/electrical-wiring.svg'; -import EvIcon from 'jsx:../static/icons/ev.svg'; -import HvacIcon from 'jsx:../static/icons/hvac.svg'; -import LawnMowerIcon from 'jsx:../static/icons/lawnmower.svg'; -import SolarIcon from 'jsx:../static/icons/solar.svg'; -import WaterHeaterIcon from 'jsx:../static/icons/water-heater.svg'; -import WeatherizationIcon from 'jsx:../static/icons/weatherization.svg'; import { ItemType } from './api/calculator-types-v1'; import { MsgFn } from './i18n/use-translated'; type ProjectInfo = { label: (msg: MsgFn) => string; - getIcon: () => React.ReactElement; items: ItemType[]; }; @@ -39,7 +28,6 @@ export const PROJECTS: Record = { clothes_dryer: { items: ['heat_pump_clothes_dryer', 'non_heat_pump_clothes_dryer'], label: msg => msg('Clothes dryer'), - getIcon: () => , }, hvac: { items: [ @@ -51,7 +39,6 @@ export const PROJECTS: Record = { 'other_heat_pump', ], label: msg => msg('Heating, ventilation & cooling'), - getIcon: () => , }, ev: { items: [ @@ -63,32 +50,26 @@ export const PROJECTS: Record = { 'ebike', ], label: msg => msg('Electric transportation'), - getIcon: () => , }, solar: { items: ['rooftop_solar_installation'], label: msg => msg('Solar', { desc: 'i.e. rooftop solar' }), - getIcon: () => , }, battery: { items: ['battery_storage_installation'], label: msg => msg('Battery storage'), - getIcon: () => , }, water_heater: { items: ['heat_pump_water_heater', 'non_heat_pump_water_heater'], label: msg => msg('Water heater'), - getIcon: () => , }, cooking: { items: ['electric_stove'], label: msg => msg('Cooking stove/range'), - getIcon: () => , }, wiring: { items: ['electric_panel', 'electric_wiring'], label: msg => msg('Electrical panel & wiring'), - getIcon: () => , }, weatherization_and_efficiency: { items: [ @@ -108,11 +89,9 @@ export const PROJECTS: Record = { 'energy_audit', ], label: msg => msg('Weatherization & efficiency'), - getIcon: () => , }, lawn_care: { items: ['electric_outdoor_equipment'], label: msg => msg('Lawn Care'), - getIcon: () => , }, }; diff --git a/src/state-incentive-details.tsx b/src/state-incentive-details.tsx index 72811f7..823f7f2 100644 --- a/src/state-incentive-details.tsx +++ b/src/state-incentive-details.tsx @@ -15,6 +15,7 @@ import { IncentiveCard } from './incentive-card'; import { IRARebate, getRebatesFor } from './ira-rebates'; import { itemName } from './item-name'; import { PartnerLogos } from './partner-logos'; +import { PROJECT_ICONS } from './project-icons'; import { PROJECTS, Project } from './projects'; import { safeLocalStorage } from './safe-local-storage'; @@ -250,7 +251,7 @@ const IncentiveGrid = forwardRef( const options: Option[] = tabs.map(({ project, count }) => ({ value: project, label: PROJECTS[project].label(msg), - getIcon: PROJECTS[project].getIcon, + getIcon: PROJECT_ICONS[project], badge: count, disabled: count === 0, })); diff --git a/static/icons/lawnmower.svg b/static/icons/lawnmower.svg index a3b20ca..5243145 100644 --- a/static/icons/lawnmower.svg +++ b/static/icons/lawnmower.svg @@ -1,5 +1,5 @@ - +