Skip to content

Commit

Permalink
First cut at RI state calculator
Browse files Browse the repository at this point in the history
## Description

- New .html page for the RI calculator

- Card-based presentation of incentives, rather than the table like in
  the IRA calculator. This follows the initial designs.

No utility incentives yet, only state + federal. Utility selection
will come in a followup.

If you enter a non-RI zip code, the API gives the error "we don't yet
have state-level coverage in <state>". This isn't exactly what we want
-- it should say "that zip/address is not in Rhode Island". This will
require an API change to add a "state" parameter.

## Test Plan

Enter a RI zip code and make sure that RI incentives come up.

**Note** this new page will not be available in the Vercel preview. I
haven't configured the Parcel build to include `rhode-island.html`
yet, because I don't want the page to be publicly available when this
lands.

## Next Steps

Follow-up PRs that add the utility and project fields.
  • Loading branch information
oyamauchi committed Aug 24, 2023
1 parent 09afa7e commit c06e712
Show file tree
Hide file tree
Showing 6 changed files with 554 additions and 0 deletions.
30 changes: 30 additions & 0 deletions src/api/calculator-types-v1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export type IncentiveType = 'tax_credit' | 'pos_rebate';
export type AuthorityType = 'federal' | 'state' | 'utility';

export type AmountType = 'dollar_amount' | 'percent' | 'dollars_per_unit';
export interface Amount {
type: AmountType;
number: number;
maximum?: number;
representative?: number;
unit?: string;
}

export interface Incentive {
type: IncentiveType;
authority_type: AuthorityType;
authority_name: string | null;
program: string;
item: string;
item_url: string;
amount: Amount;
start_date: number;
end_date: number;

eligible: boolean;
}

export interface APIResponse {
pos_rebate_incentives: Incentive[];
tax_credit_incentives: Incentive[];
}
32 changes: 32 additions & 0 deletions src/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,35 @@ export const lightningBolt = (w: number = 38, h: number = 64) => html`<svg
fill="#F9D65B"
/>
</svg>`;

export const exclamationPoint = (w: number = 16, h: number = 16) => html`<svg
xmlns="http://www.w3.org/2000/svg"
width="${w}"
height="${h}"
viewBox="0 0 16 16"
fill="none"
>
<rect width="16" height="16" rx="2" fill="#846F24" />
<rect
x="9.12494"
y="8.12549"
width="2.25"
height="5.25"
rx="1.125"
transform="rotate(-180 9.12494 8.12549)"
fill="#FEF2CA"
stroke="#FEF2CA"
stroke-width="0.25"
/>
<rect
x="9.12494"
y="13.1255"
width="2.25"
height="2.25"
rx="1.125"
transform="rotate(-180 9.12494 13.1255)"
fill="#FEF2CA"
stroke="#FEF2CA"
stroke-width="0.25"
/>
</svg>`;
77 changes: 77 additions & 0 deletions src/rhode-island.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Demo Rewiring America calculator widget."
/>
<meta name="robots" content="noindex" />
<title>Rewiring America Incentives Calculator - Rhode Island</title>
<script type="module" src="./state-calculator.ts"></script>
<link rel="stylesheet" type="text/css" href="./rewiring-fonts.css" />
<style>
html,
body {
margin: 0;
padding: 0;
background: white;
color: #222;
box-sizing: border-box;
}

main {
font-smoothing: antialiased;
-webkit-font-smoothing: antialiased;
font-family: 'GT America', system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans',
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 16px;
line-height: 28px;
font-weight: 400;
margin: 32px auto;
max-width: 1024px;
padding: 32px;
}

h1,
h2,
h3,
p {
padding: 0;
}

/* Extra small devices */
@media only screen and (max-width: 600px) {
main {
padding: 0;
}

h1,
h2,
h3,
p {
padding: 32px;
}
}
</style>
</head>

<body>
<main>
<h1>Rewiring America Incentives Calculator</h1>
<h2>Rhode Island</h2>
<rewiring-america-state-calculator
api-key="{{ apiKey }}"
state="RI"
zip="02807"
household-income="80000"
household-size="1"
tax-filing="single"
owner-status="homeowner"
></rewiring-america-state-calculator>
</main>
</body>
</html>
181 changes: 181 additions & 0 deletions src/state-calculator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { LitElement, html, nothing } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { Task, initialState } from '@lit-labs/task';
import { baseStyles, gridStyles } from './styles';
import { formTemplate, formStyles } from './calculator-form';
import { FilingStatus, OwnerStatus } from './calculator-types';
import { CALCULATOR_FOOTER } from './calculator-footer';
import { fetchApi } from './api/fetch';
import {
stateIncentivesTemplate,
stateIncentivesStyles,
cardStyles,
} from './state-incentive-details';

const loadingTemplate = () => html`
<div class="card card-content">Loading...</div>
`;

const errorTemplate = (error: unknown) => html`
<div class="card card-content">
${typeof error === 'object' && error && 'message' in error && error.message
? error.message
: 'Error loading incentives.'}
</div>
`;

const DEFAULT_CALCULATOR_API_HOST: string = 'https://api.rewiringamerica.org';

@customElement('rewiring-america-state-calculator')
export class RewiringAmericaStateCalculator extends LitElement {
static override styles = [
baseStyles,
cardStyles,
gridStyles,
...formStyles,
stateIncentivesStyles,
];

/* supported properties to control showing/hiding of each card in the widget */

@property({ type: Boolean, attribute: 'hide-form' })
hideForm: boolean = false;

@property({ type: Boolean, attribute: 'hide-details' })
hideDetails: boolean = false;

/* supported properties to control which API path and key is used to load the calculator results */

@property({ type: String, attribute: 'api-key' })
apiKey: string = '';

@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' })
zip: string = '';

@property({ type: String, attribute: 'owner-status' })
ownerStatus: OwnerStatus = 'homeowner';

@property({ type: String, attribute: 'household-income' })
householdIncome: string = '0';

@property({ type: String, attribute: 'tax-filing' })
taxFiling: FilingStatus = 'single';

@property({ type: String, attribute: 'household-size' })
householdSize: string = '1';

submit(e: SubmitEvent) {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
this.zip = (formData.get('zip') as string) || '';
this.ownerStatus = (formData.get('owner_status') as OwnerStatus) || '';
this.householdIncome = (formData.get('household_income') as string) || '';
this.taxFiling = (formData.get('tax_filing') as FilingStatus) || '';
this.householdSize = (formData.get('household_size') as string) || '';
}

get hideResult() {
return !(
this.zip &&
this.ownerStatus &&
this.taxFiling &&
this.householdIncome &&
this.householdSize
);
}

private _task = new Task(this, {
// this array of parameters is generated by the `args` function below
// it's formatted with snake_case to make it really easy to throw into URLSearchParams
task: async ([
zip,
owner_status,
household_income,
tax_filing,
household_size,
]) => {
if (this.hideResult) {
// this is a special response type provided by Task to keep it in the INITIAL state
return initialState;
}
const query = new URLSearchParams({
'location[zip]': zip,
owner_status,
household_income,
tax_filing,
household_size,
});
query.append('authority_types', 'federal');
query.append('authority_types', 'state');

return await fetchApi(
this.apiKey,
this.apiHost,
'/api/v1/calculator',
query,
);
},
// if the args array changes then the task will run again
args: () => [
this.zip,
this.ownerStatus,
this.householdIncome,
this.taxFiling,
this.householdSize,
],
});

override render() {
return html`
<div class="calculator">
<div class="card card-content">
<h1>Your household info</h1>
${this.hideForm
? nothing
: formTemplate(
[
this.zip,
this.ownerStatus,
this.householdIncome,
this.taxFiling,
this.householdSize,
],
(event: SubmitEvent) => this.submit(event),
)}
</div>
${this.hideResult
? nothing
: html`
${this._task.render({
pending: loadingTemplate,
complete: results => stateIncentivesTemplate(results),
error: errorTemplate,
})}
`}
${CALCULATOR_FOOTER}
</div>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
'rewiring-america-state-calculator': RewiringAmericaStateCalculator;
}
}
Loading

0 comments on commit c06e712

Please sign in to comment.