diff --git a/src/components/common/DynamicList.tsx b/src/components/common/DynamicList.tsx new file mode 100644 index 0000000000..d3e644640e --- /dev/null +++ b/src/components/common/DynamicList.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { DynamicListProps } from "./PropTypes"; +import { v1 as uuid } from "uuid"; +import { isFunction } from "lodash"; + +interface DynamicListState { + /** An array of objects that represent the state of the child components. */ + listItems: Array; + /** An array of UUID keys mapped 1:1 with listItems, used to indicate + * when React should re-render. + */ + keys: Array; +} + +/** + * A component that can generate an arbitrary number + * of instances of a component provided by the caller. + */ +export default class DynamicList extends React.Component< + DynamicListProps, + DynamicListState +> { + /** + * Initialize component and its state. + * @param {DynamicListProps} props + */ + constructor(props: DynamicListProps) { + super(props); + this.state = { + listItems: [], + keys: [], + }; + } + + /** + * Removes the child component instance at the provided index. + * @param {number} index + */ + deleteItem(index: number): void { + const updatedListItems = [...this.state.listItems]; + updatedListItems.splice(index, 1); + + const updatedKeys = [...this.state.keys]; + updatedKeys.splice(index, 1); + + this.setState({ listItems: updatedListItems, keys: updatedKeys }); + this.props.onListItemsUpdated(this.state.listItems); + } + + /** + * Creates a new child component instance. + */ + addItem(): void { + const updatedListItems = [ + ...this.state.listItems, + this.props.modelFactory(), + ]; + const updatedKeys = [...this.state.keys, uuid()]; + + this.setState({ listItems: updatedListItems, keys: updatedKeys }); + this.props.onListItemsUpdated(this.state.listItems); + } + + /** + * Replaces the child component state at the provided + * index with the provided value. + * @param {number} index the index of the child instance to update + * @param {TModel} value the new state value + */ + updateItem(index: number, value: TModel): void { + const updatedModel = [...this.state.listItems]; + updatedModel[index] = value; + this.setState({ listItems: updatedModel }); + this.props.onListItemsUpdated(this.state.listItems); + } + + /** + * Renders each child instance using the component provided in the caller. + * @return {React.ReactNode} the rendered child components + */ + renderChildren(): React.ReactNode { + return this.state.listItems.map((entry: TModel, index: number) => { + return ( +
+ + {this.props.renderListItem((value: TModel) => + this.updateItem(index, value) + )} +
+ ); + }); + } + + /** + * React render method. + * @return {React.ReactNode} the rendered component + */ + render(): React.ReactNode { + return ( + <> + {this.renderChildren()} + + + ); + } +} diff --git a/src/components/common/PropTypes.ts b/src/components/common/PropTypes.ts new file mode 100644 index 0000000000..4839e3119d --- /dev/null +++ b/src/components/common/PropTypes.ts @@ -0,0 +1,6 @@ +export interface DynamicListProps { + addText: string | ((listItems: Array) => string); + modelFactory: () => TModel; + onListItemsUpdated: (value: Array) => void; + renderListItem: (onListItemUpdated: (value: TModel) => void) => JSX.Element; +} diff --git a/src/components/common/index.ts b/src/components/common/index.ts new file mode 100644 index 0000000000..4fd7acd3a6 --- /dev/null +++ b/src/components/common/index.ts @@ -0,0 +1,5 @@ +export { default as Footer } from "./Footer"; +export { default as Header } from "./Header"; +export { default as Layout } from "./Layout"; +export { default as DynamicList } from "./DynamicList"; +export * from "./PropTypes"; diff --git a/src/components/letter/AddressInput.tsx b/src/components/letter/AddressInput.tsx new file mode 100644 index 0000000000..54fa6b1c08 --- /dev/null +++ b/src/components/letter/AddressInput.tsx @@ -0,0 +1,110 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import React, { useState, ReactElement } from "react"; + +import { LobAddress } from "./LetterTypes"; + +type AddressInputProps = { + /** callback function for every time the address is updated */ + onAddressUpdated: (a: LobAddress) => void; +}; + +/** Renders input fields for the user's address + * + * @return {ReactElement} the rendered component + */ +export default function AddressInput({ + onAddressUpdated: onAddressUpdated, +}: AddressInputProps): ReactElement { + const [address, setAddress] = useState({} as LobAddress); + + /** Callback for when part of user's address is updated. + * adds it to the full address in our internal state and + * calls the parent's callback + * + * @param {Address} addressPatch the updated address + */ + function updateAddress(addressPatch: Partial) { + const updatedAddress = { ...address, ...addressPatch }; + setAddress(updatedAddress); + onAddressUpdated(updatedAddress); + } + + return ( + <> +
+
+ + + updateAddress({ + name: e.target.value, + }) + } + /> +
+
+ +
+
+ + + + updateAddress({ + address_line1: e.target.value, + }) + } + /> + +
+
+ + updateAddress({ + address_city: e.target.value, + }) + } + /> +
+
+ + updateAddress({ + address_state: e.target.value, + }) + } + /> +
+ +
+ + updateAddress({ + address_zip: e.target.value, + }) + } + /> +
+
+
+
+ + ); +} diff --git a/src/components/letter/LetterForm.tsx b/src/components/letter/LetterForm.tsx index fe103b1973..7bf70d947f 100644 --- a/src/components/letter/LetterForm.tsx +++ b/src/components/letter/LetterForm.tsx @@ -9,11 +9,13 @@ import "purecss/build/grids-responsive-min.css"; import { Template, LobAddress } from "./LetterTypes"; import CheckoutForm from "./CheckoutForm"; import MyAddressInput from "./MyAddressInput"; +import AddressInput from "./AddressInput"; import { CombinedOfficialFetchingService } from "../../services/CombinedOfficialFetchingService"; import { OfficialAddressCheckboxList } from "./OfficialAddressCheckboxList"; import { TemplateInputs } from "./TemplateInputs"; import { OfficialAddress } from "../../services/OfficialTypes"; import { lobAddressToSingleLine } from "./LobAddressUtils"; +import { DynamicList } from "../common"; const SpecialVars = ["YOUR NAME"]; // , "YOUR DISTRICT"]; @@ -65,6 +67,9 @@ function LetterForm({ template, googleApiKey }: LetterFormProps): ReactElement { const [myAddress, setMyAddress] = useState({} as LobAddress); const [variableMap, setVariableMap] = useState({} as Record); const [checkedAddresses, setCheckedAddresses] = useState([] as LobAddress[]); + const [additionalAddresses, setAdditionalAddresses] = useState( + [] as LobAddress[] + ); const [officials, setOfficials] = useState([] as OfficialAddress[]); const [isSearching, setIsSearching] = useState(false); @@ -214,8 +219,21 @@ function LetterForm({ template, googleApiKey }: LetterFormProps): ReactElement { /> )} + + addresses.length === 0 + ? "Add Missing Representative" + : "Add Another" + } + onListItemsUpdated={setAdditionalAddresses} + modelFactory={() => ({} as LobAddress)} + renderListItem={(onListItemUpdated: (a: LobAddress) => void) => ( + + )} + /> + No representatives found, sorry; - } - return ( -
- {officialAddresses?.map((officialAddress) => ( - - ))} -
+ <> + {myAddress.address_line1 && officialAddresses.length === 0 ? ( + <> +
No representatives found, sorry
+
+ + ) : ( +
+ {officialAddresses?.map((officialAddress) => ( + + ))} +
+ )} + + + ); }