Skip to content

Commit

Permalink
Refactor logic for HEAR multi-item incentive names (#188)
Browse files Browse the repository at this point in the history
## 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).
  • Loading branch information
oyamauchi authored Sep 28, 2024
1 parent 4928171 commit 1c6aa13
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 70 deletions.
63 changes: 16 additions & 47 deletions src/item-name.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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',
Expand Down Expand Up @@ -116,17 +115,13 @@ const ITEM_GROUPS: { group: ItemGroup; members: Set<ItemType> }[] = [
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<ItemType>) => {
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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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]"' });
Expand Down
24 changes: 24 additions & 0 deletions src/project-icons.tsx
Original file line number Diff line number Diff line change
@@ -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: () => <ClothesDryerIcon width="1em" />,
hvac: () => <HvacIcon width="1em" />,
ev: () => <EvIcon width="1em" />,
solar: () => <SolarIcon width="1em" />,
battery: () => <BatteryIcon width="1em" />,
water_heater: () => <WaterHeaterIcon width="1em" />,
cooking: () => <CookingIcon width="1em" />,
wiring: () => <ElectricalWiringIcon width="1em" />,
weatherization_and_efficiency: () => <WeatherizationIcon width="1em" />,
lawn_care: () => <LawnMowerIcon width="1em" />,
};
21 changes: 0 additions & 21 deletions src/projects.tsx → src/projects.ts
Original file line number Diff line number Diff line change
@@ -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[];
};

Expand All @@ -39,7 +28,6 @@ export const PROJECTS: Record<Project, ProjectInfo> = {
clothes_dryer: {
items: ['heat_pump_clothes_dryer', 'non_heat_pump_clothes_dryer'],
label: msg => msg('Clothes dryer'),
getIcon: () => <ClothesDryerIcon width="1em" />,
},
hvac: {
items: [
Expand All @@ -51,7 +39,6 @@ export const PROJECTS: Record<Project, ProjectInfo> = {
'other_heat_pump',
],
label: msg => msg('Heating, ventilation & cooling'),
getIcon: () => <HvacIcon width="1em" />,
},
ev: {
items: [
Expand All @@ -63,32 +50,26 @@ export const PROJECTS: Record<Project, ProjectInfo> = {
'ebike',
],
label: msg => msg('Electric transportation'),
getIcon: () => <EvIcon width="1em" />,
},
solar: {
items: ['rooftop_solar_installation'],
label: msg => msg('Solar', { desc: 'i.e. rooftop solar' }),
getIcon: () => <SolarIcon width="1em" />,
},
battery: {
items: ['battery_storage_installation'],
label: msg => msg('Battery storage'),
getIcon: () => <BatteryIcon width="1em" />,
},
water_heater: {
items: ['heat_pump_water_heater', 'non_heat_pump_water_heater'],
label: msg => msg('Water heater'),
getIcon: () => <WaterHeaterIcon width="1em" />,
},
cooking: {
items: ['electric_stove'],
label: msg => msg('Cooking stove/range'),
getIcon: () => <CookingIcon width="1em" />,
},
wiring: {
items: ['electric_panel', 'electric_wiring'],
label: msg => msg('Electrical panel & wiring'),
getIcon: () => <ElectricalWiringIcon width="1em" />,
},
weatherization_and_efficiency: {
items: [
Expand All @@ -108,11 +89,9 @@ export const PROJECTS: Record<Project, ProjectInfo> = {
'energy_audit',
],
label: msg => msg('Weatherization & efficiency'),
getIcon: () => <WeatherizationIcon width="1em" />,
},
lawn_care: {
items: ['electric_outdoor_equipment'],
label: msg => msg('Lawn Care'),
getIcon: () => <LawnMowerIcon width="1em" height="1em" />,
},
};
3 changes: 2 additions & 1 deletion src/state-incentive-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -250,7 +251,7 @@ const IncentiveGrid = forwardRef<HTMLDivElement, IncentiveGridProps>(
const options: Option<Project>[] = tabs.map(({ project, count }) => ({
value: project,
label: PROJECTS[project].label(msg),
getIcon: PROJECTS[project].getIcon,
getIcon: PROJECT_ICONS[project],
badge: count,
disabled: count === 0,
}));
Expand Down
2 changes: 1 addition & 1 deletion static/icons/lawnmower.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 1c6aa13

Please sign in to comment.