Skip to content

Commit

Permalink
Merge main
Browse files Browse the repository at this point in the history
  • Loading branch information
ayangster committed Oct 2, 2023
2 parents a356ffe + f6a8672 commit 6eb9f07
Show file tree
Hide file tree
Showing 12 changed files with 449 additions and 76 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ yarn-error.log*

# parcel
.parcel-cache

# cypress
cypress/videos/*.mp4
1 change: 1 addition & 0 deletions .husky/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_
8 changes: 8 additions & 0 deletions .husky/post-checkout
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash

set -e

CHANGED=$(git diff "$1" "$2" --stat -- ./yarn.lock | wc -l)
if (( CHANGED > 0 )); then
echo "📦 yarn.lock changed. Run yarn install to bring your dependencies up to date."
fi
5 changes: 5 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx git-branch-is --not main
npx lint-staged
54 changes: 54 additions & 0 deletions cypress/e2e/state-calculator.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/// <reference types="Cypress" />

describe('template spec', () => {
it('passes', () => {
cy.visit('http://localhost:1234/rhode-island.html');
cy.get('rewiring-america-state-calculator').should('exist');

cy.get('rewiring-america-state-calculator')
.shadow()
.contains('Your household info');

cy.get('rewiring-america-state-calculator')
.shadow()
.find('input#zip')
.type('02859{enter}');

cy.get('rewiring-america-state-calculator')
.shadow()
.contains('Incentives available to you in Rhode Island');

cy.get('rewiring-america-state-calculator')
.shadow()
.find('select#utility')
.should("exist");

cy.get('rewiring-america-state-calculator')
.shadow()
.contains("Incentives you're interested in");

cy.get('rewiring-america-state-calculator')
.shadow()
.contains('$8,000 off a heat pump');

cy.get('rewiring-america-state-calculator')
.shadow()
.contains('$1,250/ton off a heat pump');

cy.get('rewiring-america-state-calculator')
.shadow()
.contains('$350/ton off a heat pump');

cy.get('rewiring-america-state-calculator')
.shadow()
.contains('30% of cost of geothermal heating installation');

cy.get('rewiring-america-state-calculator')
.shadow()
.contains('$2,000 off a heat pump');

cy.get('rewiring-america-state-calculator')
.shadow()
.contains("Other incentives available to you");
});
});
Binary file removed cypress/videos/calculator.cy.ts.mp4
Binary file not shown.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"serve:widget": "parcel serve ./src/*.html --dist-dir build",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"lint": "prettier --check . && eslint ."
"lint": "prettier --check . && eslint .",
"prepare": "husky install"
},
"browserslist": {
"production": [
Expand All @@ -34,6 +35,8 @@
"@typescript-eslint/parser": "^6.1.0",
"cypress": "^12.6.0",
"eslint": "^8.45.0",
"husky": "^8.0.3",
"lint-staged": "^14.0.1",
"parcel": "v2.8.3",
"postcss": "^8.4.21",
"postcss-modules": "^6.0.0",
Expand All @@ -59,5 +62,8 @@
"sourceMap": false,
"publicUrl": "./"
}
},
"lint-staged": {
"**/*.{js,ts,json,md,css,html}": "prettier --write"
}
}
10 changes: 10 additions & 0 deletions src/api/calculator-types-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ export interface Incentive {
eligible: boolean;
}

export type APIUtilitiesResponse = {
[utilityId: string]: {
name: string;
};
};

export interface APIResponse {
authorities: {
[authorityId: string]: {
Expand All @@ -63,6 +69,10 @@ export interface APIResponse {
};
};
};
coverage: {
state: string | null;
utility: string | null;
};
savings: {
tax_credit: number;
pos_rebate: number;
Expand Down
4 changes: 2 additions & 2 deletions src/api/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
* Fetches a response from the Incentives API. Handles turning an error response
* into an exception with a useful message.
*/
export async function fetchApi(
export async function fetchApi<R>(
apiKey: string,
apiHost: string,
path: string,
query: URLSearchParams,
) {
): Promise<R> {
const url = new URL(apiHost);
url.pathname = path;
url.search = query.toString();
Expand Down
1 change: 0 additions & 1 deletion src/rhode-island.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@
<h1>Rewiring America Incentives Calculator</h1>
<rewiring-america-state-calculator
api-key="{{ apiKey }}"
state="RI"
household-income="80000"
household-size="1"
tax-filing="single"
Expand Down
175 changes: 113 additions & 62 deletions src/state-calculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { iconTabBarStyles } from './icon-tab-bar';
import '@shoelace-style/shoelace/dist/components/spinner/spinner';
import { STATES } from './states';
import { authorityLogosStyles } from './authority-logos';
import { APIResponse, APIUtilitiesResponse } from './api/calculator-types-v1';

const loadingTemplate = () => html`
<div class="card card-content">
Expand Down Expand Up @@ -70,17 +71,6 @@ export class RewiringAmericaStateCalculator extends LitElement {
@property({ type: String, attribute: 'api-host' })
apiHost: string = DEFAULT_CALCULATOR_API_HOST;

/**
* Property to customize the calculator for a particular state. Must be the
* two-letter code, uppercase (example: "NY").
*
* Currently the only customization is to display the name of the state.
* TODO: Have a nice error message if you enter a zip/address outside this
* state, if it's defined.
*/
@property({ type: String, attribute: 'state' })
state: string = '';

/* supported properties to allow pre-filling the form */

@property({ type: String, attribute: 'zip' })
Expand Down Expand Up @@ -110,6 +100,24 @@ export class RewiringAmericaStateCalculator extends LitElement {
@property({ type: String })
selectedOtherTab: Project = 'battery';

/**
* This is a hack to deal with a quirk of the UI.
*
* Specifically:
*
* - Rendering the utility selector / map outline requires knowing what state
* the user is in, to know which outline to show.
* - That state is unknown until the /calculator response is available.
* - When the user changes the utility selector, the /calculator response is
* unavailable while it's loading. But we want to continue showing the
* utility selector / map outline.
*
* This property thus temporarily remembers the state from the last completed
* /calculator response, when the utility selector is changed. It's cleared
* when the /calculator response arrives.
*/
tempState: string | null = null;

submit(e: SubmitEvent) {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
Expand Down Expand Up @@ -177,22 +185,33 @@ export class RewiringAmericaStateCalculator extends LitElement {
const query = new URLSearchParams({
'location[zip]': this.zip,
});
const utilityMap = await fetchApi(
this.apiKey,
this.apiHost,
'/api/v1/utilities',
query,
);

return Object.keys(utilityMap).map(id => ({
value: id,
label: utilityMap[id].name,
}));
try {
const utilityMap = await fetchApi<APIUtilitiesResponse>(
this.apiKey,
this.apiHost,
'/api/v1/utilities',
query,
);

return Object.keys(utilityMap).map(id => ({
value: id,
label: utilityMap[id].name,
}));
} catch (_) {
// Just use an empty utilities list if there's an error.
return [];
}
},
onComplete: options => {
// Preserve the previous utility selection if it's still available.
if (!options.map(o => o.value).includes(this.utility)) {
this.utility = options[0].value;
if (options.length === 0) {
this.utility = '';
} else {
// Preserve the previous utility selection if it's still available.
// Select the first option in the list otherwise.
if (!options.map(o => o.value).includes(this.utility)) {
this.utility = options[0].value;
}
}
this._task.run();
},
Expand All @@ -213,21 +232,47 @@ export class RewiringAmericaStateCalculator extends LitElement {
tax_filing: this.taxFiling,
household_size: this.householdSize,
});
query.append('authority_types', 'federal');
query.append('authority_types', 'state');
query.append('authority_types', 'utility');
query.set('utility', this.utility);
if (this.utility) {
query.set('utility', this.utility);
}

return await fetchApi(
return fetchApi<APIResponse>(
this.apiKey,
this.apiHost,
'/api/v1/calculator',
query,
);
},
onComplete: () => {
this.tempState = null;
},
});

override render() {
// If we have incentives loaded, use coverage.state from that to determine
// which state outline to show. Otherwise, look at the "tempState" override,
// which is set when the utility selector is changed.
const highlightedState =
this._task.status === TaskStatus.COMPLETE
? this._task.value?.coverage.state
: this.tempState;

// Show the following elements below the form:
//
// - The utility selector/map, if we know what state the user's ZIP is
// located in, we have an outline map of that state, and the options for
// utilities are finished loading.
//
// - The incentive results with a separator line above, if both the
// incentives and utilities are finished loading.
//
// - The loading spinner, if either utilities or incentives are still
// loading.

const showLoading =
this._utilitiesTask.status === TaskStatus.PENDING ||
this._task.status === TaskStatus.PENDING;

return html`
<div class="calculator">
<div class="card card-content">
Expand All @@ -248,40 +293,46 @@ export class RewiringAmericaStateCalculator extends LitElement {
'grid-3-2-1',
)}
</div>
${this._utilitiesTask.render({
pending: loadingTemplate,
complete: options =>
utilitySelectorTemplate(
STATES[this.state],
this.utility,
options,
newUtility => {
this.utility = newUtility;
this._task.run();
},
),
error: errorTemplate,
})}
${this._task.status !== TaskStatus.INITIAL &&
this._utilitiesTask.status === TaskStatus.COMPLETE
? html`<div class="separator"></div>`
${
// This is defensive against the possibility that the backend and
// frontend have support for different sets of states. (Backend support
// means knowing utilities and incentives for a state; frontend support
// means having an outline map and name for a state.)
this._utilitiesTask.status === TaskStatus.COMPLETE &&
this._utilitiesTask.value!.length > 0 &&
highlightedState &&
highlightedState in STATES
? utilitySelectorTemplate(
STATES[highlightedState],
this.utility,
this._utilitiesTask.value!,
newUtility => {
this.utility = newUtility;
this.tempState = highlightedState;
this._task.run();
},
)
: nothing
}
${this._utilitiesTask.status === TaskStatus.COMPLETE &&
this._task.status === TaskStatus.COMPLETE
? [
html`<div class="separator"></div>`,
stateIncentivesTemplate(
this._task.value!,
this.projects,
newOtherSelection =>
(this.selectedOtherTab = newOtherSelection),
newSelection => (this.selectedProjectTab = newSelection),
this.selectedOtherTab,
this.selectedProjectTab,
),
]
: nothing}
${showLoading ? loadingTemplate() : nothing}
${this._task.status === TaskStatus.ERROR && !showLoading
? errorTemplate(this._task.error)
: nothing}
${this._task.render({
pending: loadingTemplate,
complete: results =>
this._utilitiesTask.status !== TaskStatus.COMPLETE
? nothing
: stateIncentivesTemplate(
results,
this.projects,
newOtherSelection =>
(this.selectedOtherTab = newOtherSelection),
newSelection => (this.selectedProjectTab = newSelection),
this.selectedOtherTab,
this.selectedProjectTab,
),
error: errorTemplate,
})}
${CALCULATOR_FOOTER}
</div>
`;
Expand Down
Loading

0 comments on commit 6eb9f07

Please sign in to comment.