From f2b242e29ac9eaa9d346e6adb838823f923d1e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20Harnes?= Date: Tue, 12 Jan 2021 20:50:45 +0100 Subject: [PATCH] Adds a standardized input component and fixes validation of input data POC --- .eslintrc.json | 1 + .../panes/DonationPane/DonationPane.style.tsx | 5 + .../panes/DonationPane/DonationPane.tsx | 85 +++------ .../panes/DonationPane/ShareSelection.tsx | 179 ++++-------------- .../panes/DonationPane/SharesSum.tsx | 12 ++ src/components/panes/DonorPane/DonorPane.tsx | 9 +- .../panes/MethodPane/MethodPane.style.tsx | 5 + .../panes/MethodPane/MethodPane.tsx | 37 ++-- .../shared/Buttons/NavigationButtons.tsx | 117 ------------ .../shared/Input/TextInput.style.tsx | 93 +++++++++ src/components/shared/Input/TextInput.tsx | 34 ++++ .../shared/RichSelect/RichSelect.style.tsx | 2 +- .../RichSelect/RichSelectOption.style.tsx | 4 +- src/store/donation/actions.ts | 26 ++- src/store/donation/reducer.ts | 71 +++++-- src/store/donation/saga.ts | 25 ++- src/store/donation/types.ts | 26 ++- src/store/layout/actions.ts | 23 +-- src/store/layout/reducer.ts | 8 - src/store/layout/saga.ts | 4 +- src/store/layout/types.ts | 20 -- src/store/state.ts | 10 +- src/types/Temp.ts | 3 +- 23 files changed, 379 insertions(+), 420 deletions(-) create mode 100644 src/components/panes/DonationPane/DonationPane.style.tsx create mode 100644 src/components/panes/DonationPane/SharesSum.tsx delete mode 100644 src/components/shared/Buttons/NavigationButtons.tsx create mode 100644 src/components/shared/Input/TextInput.style.tsx create mode 100644 src/components/shared/Input/TextInput.tsx diff --git a/.eslintrc.json b/.eslintrc.json index 00348c5..13db5ca 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -40,6 +40,7 @@ "@typescript-eslint/no-explicit-any": "error", "radix": "off", "import/prefer-default-export": "off", + "react/jsx-props-no-spreading": "off", "react/prop-types": "off", "no-param-reassign": "off", "no-console": [ diff --git a/src/components/panes/DonationPane/DonationPane.style.tsx b/src/components/panes/DonationPane/DonationPane.style.tsx new file mode 100644 index 0000000..288b7d0 --- /dev/null +++ b/src/components/panes/DonationPane/DonationPane.style.tsx @@ -0,0 +1,5 @@ +import styled from "styled-components"; + +export const SumWrapper = styled.div` + padding-bottom: 14px; +`; diff --git a/src/components/panes/DonationPane/DonationPane.tsx b/src/components/panes/DonationPane/DonationPane.tsx index a7a59ed..baad984 100644 --- a/src/components/panes/DonationPane/DonationPane.tsx +++ b/src/components/panes/DonationPane/DonationPane.tsx @@ -1,22 +1,22 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions */ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useForm } from "react-hook-form"; import Validator from "validator"; import { registerDonationAction, setSum, + setShareType, } from "../../../store/donation/actions"; -import { setShareType } from "../../../store/layout/actions"; import { Pane, PaneContainer } from "../Panes.style"; import { State } from "../../../store/state"; -import { TextField } from "../Forms.style"; -import ErrorField from "../../shared/Error/ErrorField"; import { PaymentMethod, ShareType } from "../../../types/Enums"; import { RichSelectOption } from "../../shared/RichSelect/RichSelectOption"; import { RichSelect } from "../../shared/RichSelect/RichSelect"; import { NextButton } from "../../shared/Buttons/NavigationButtons.style"; import { SharesSelection } from "./ShareSelection"; +import { TextInput } from "../../shared/Input/TextInput"; +import { SumWrapper } from "./DonationPane.style"; +import { SharesSum } from "./SharesSum"; interface DonationFormValues { recurring: string; @@ -24,18 +24,11 @@ interface DonationFormValues { sum: string; } -// TODO: Add loading animation after submitting - export const DonationPane: React.FC = () => { const dispatch = useDispatch(); - const [nextDisabled, setNextDisabled] = useState(false); - const [sumErrorAnimation, setSumErrorAnimation] = useState(false); - const shareType = useSelector((state: State) => state.layout.shareType); + const shareType = useSelector((state: State) => state.donation.shareType); const donationMethod = useSelector((state: State) => state.donation.method); - const donor = useSelector((state: State) => state.donation.donor); - const currentPaymentMethod = useSelector( - (state: State) => state.donation.method - ); + const donationValid = useSelector((state: State) => state.donation.isValid); const { register, @@ -46,13 +39,10 @@ export const DonationPane: React.FC = () => { const watchAllFields = watch(); useEffect(() => { - errors.sum ? setSumErrorAnimation(true) : setSumErrorAnimation(false); - - if (Object.keys(errors).length === 0) { - setNextDisabled(false); - } else { - setNextDisabled(true); - } + /** + * TODO: + * Handle errors, set donation valid + */ const values = watchAllFields; if (values.sum) @@ -60,45 +50,29 @@ export const DonationPane: React.FC = () => { }, [dispatch, errors, watchAllFields]); function onSubmit() { - if (Object.keys(errors).length === 0) { - if (donor) { - if ( - donor.name && - donor.email && - donor.newsletter !== undefined && - currentPaymentMethod - ) { - dispatch(registerDonationAction.started(undefined)); - } - } - } - } - - let sumField = null; - if ( - donationMethod === PaymentMethod.PAYPAL || - donationMethod === PaymentMethod.VIPPS - ) { - sumField = ( - Validator.isInt(val) && val > 0, - })} - /> - ); + dispatch(registerDonationAction.started(undefined)); } return (
- {sumErrorAnimation && } - {sumField} + {(donationMethod === PaymentMethod.VIPPS || + donationMethod === PaymentMethod.PAYPAL) && ( + + Validator.isInt(val) && val > 0, + })} + /> + + )} { value={ShareType.CUSTOM} > + - + Neste diff --git a/src/components/panes/DonationPane/ShareSelection.tsx b/src/components/panes/DonationPane/ShareSelection.tsx index 2dd1fea..1f7bbf3 100644 --- a/src/components/panes/DonationPane/ShareSelection.tsx +++ b/src/components/panes/DonationPane/ShareSelection.tsx @@ -1,170 +1,59 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { useForm } from "react-hook-form"; import { useDispatch, useSelector } from "react-redux"; +import Validator from "validator"; import { Organization } from "../../../types/Organization"; import { setShares } from "../../../store/donation/actions"; import { State } from "../../../store/state"; -import { ToolTip } from "../../shared/ToolTip/ToolTip"; - -import { - OrganizationName, - PercentageText, - ShareInput, - ShareInputContainer, -} from "./ShareSelection.style"; +import { TextInput } from "../../shared/Input/TextInput"; import { OrganizationShare } from "../../../types/Temp"; -const tooltipLink = "https://gieffektivt.no/organisasjoner"; - export const SharesSelection: React.FC = () => { const dispatch = useDispatch(); - const [submitLoading, setSubmitLoading] = useState(false); - const donorName = useSelector((state: State) => state.donation.donor?.name); - const donorEmail = useSelector((state: State) => state.donation.donor?.email); - // const donorSSN = useSelector((state: State) => state.donation.donor?.ssn); - const donorNewsletter = useSelector( - (state: State) => state.donation.donor?.newsletter - ); - // const donationSum = useSelector((state: State) => state.donation.sum); - const donationMethod = useSelector((state: State) => state.donation.method); const organizations = useSelector( (state: State) => state.layout.organizations ); - const [percentageErrorAnimation, setPercentageErrorAnimation] = useState( - false - ); - const { register, watch, handleSubmit, setValue } = useForm({ mode: "all" }); + const { register, watch } = useForm({ mode: "all" }); const watchAllFields = watch(); - function getTotalPercentage() { - let totalPercentage = 0; - let detectedNegativeShare = false; - Object.keys(watchAllFields).forEach((property) => { - const share = watchAllFields[property]; - - if (share !== "") totalPercentage += parseInt(watchAllFields[property]); - if (share === "0") setValue(property, ""); - if (parseInt(watchAllFields[property], 10) < 0) - detectedNegativeShare = true; - }); - return { - totalPercentage, - detectedNegativeShare, - }; - } - - function setupOrganizationInput(org: Organization) { - return ( - -
- {org.name} - -
-
- - % -
-
- ); - } - - useEffect(() => { - const total = getTotalPercentage().totalPercentage; - const negative = getTotalPercentage().detectedNegativeShare; - if (total === 100) { - // setNextDisabled(false); - setPercentageErrorAnimation(false); - } else if (organizations) { - // setNextDisabled(true); - setPercentageErrorAnimation(true); - } - if (negative) { - // setNextDisabled(true); - setPercentageErrorAnimation(true); - } - }, [watchAllFields]); - /** - * Check if donation is valid on input and set state + * TODO: + * Extract mappings to _mapping.ts */ - function onSubmit() { - dispatch(setShares(watchAllFields)); - if (getTotalPercentage().totalPercentage === 100) { - setSubmitLoading(true); - if ( - donorName && - donorEmail && - donationMethod && - donorNewsletter !== undefined - ) { - const orgShares: Array = []; - - Object.keys(watchAllFields).forEach((property) => { - const Share = watchAllFields[property]; - if (Share > 0 && organizations) { - const orgShare: OrganizationShare = { id: 0, share: 0, name: "" }; - orgShare.id = parseInt(property); - orgShare.share = parseInt(watchAllFields[property]); - organizations.forEach((org: Organization) => { - if (orgShare.id === org.id) { - orgShare.name = org.name; - } - }); - orgShares.push(orgShare); - } - }); - - /* - const postData: DonationData = { - donor: { - name: donorName, - email: donorEmail, - newsletter: donorNewsletter, - }, - // TODO: Send payment method as string (not number) - method: "", - organizations: orgShares, - }; - if (donationSum) postData.amount = donationSum; - if (donorSSN) postData.donor.ssn = donorSSN.toString(); - - // TODO: Move dispatches from network.ts to here - postDonation(postData, dispatch).then(() => { - setSubmitLoading(false); - }); - */ - } + useEffect(() => { + if (Object.keys(watchAllFields).length > 0) { + const shares = Object.keys(watchAllFields).map( + (key): OrganizationShare => ({ + id: parseInt(key), + share: Validator.isInt(watchAllFields[key]) + ? parseInt(watchAllFields[key]) + : 0, + }) + ); + dispatch(setShares(shares)); } - } + }, [watchAllFields]); if (!organizations) return
Ingen organisasjoner
; return (
- {!submitLoading ? ( -
-
- {organizations.map((org: Organization) => - setupOrganizationInput(org) - )} -
- {percentageErrorAnimation && ( -

- Du har fordelt - {`${getTotalPercentage().totalPercentage} / 100%`} -

- )} -
- ) : ( -

Laster...

- )} +
+
+ {organizations.map((org: Organization) => ( + + ))} +
+
); }; diff --git a/src/components/panes/DonationPane/SharesSum.tsx b/src/components/panes/DonationPane/SharesSum.tsx new file mode 100644 index 0000000..82cf931 --- /dev/null +++ b/src/components/panes/DonationPane/SharesSum.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { useSelector } from "react-redux"; +import { State } from "../../../store/state"; + +export const SharesSum: React.FC = () => { + const shares = useSelector((state: State) => state.donation.shares); + const sum = shares.reduce((acc, curr) => acc + curr.share, 0); + + if (sum === 100) return null; + + return

{`Du har fordelt ${sum} / 100%`}

; +}; diff --git a/src/components/panes/DonorPane/DonorPane.tsx b/src/components/panes/DonorPane/DonorPane.tsx index a2c3ff8..3a8bf63 100644 --- a/src/components/panes/DonorPane/DonorPane.tsx +++ b/src/components/panes/DonorPane/DonorPane.tsx @@ -20,6 +20,7 @@ import { DonorType } from "../../../types/Temp"; import { RichSelectOption } from "../../shared/RichSelect/RichSelectOption"; import { NextButton } from "../../shared/Buttons/NavigationButtons.style"; import { nextPane } from "../../../store/layout/actions"; +import { TextInput } from "../../shared/Input/TextInput"; interface DonorFormValues extends DonorInput { privacyPolicy: boolean; @@ -110,18 +111,18 @@ export const DonorPane: React.FC = () => { {nameErrorAnimation && } - {emailErrorAnimation && } - Validate.isEmail(val), })} diff --git a/src/components/panes/MethodPane/MethodPane.style.tsx b/src/components/panes/MethodPane/MethodPane.style.tsx index 081f437..17ba2e4 100644 --- a/src/components/panes/MethodPane/MethodPane.style.tsx +++ b/src/components/panes/MethodPane/MethodPane.style.tsx @@ -74,3 +74,8 @@ export const MethodButton = styled.div` background-size: contain; } `; + +export const RecurringSelectWrapper = styled.div` + padding-top: 10px; + padding-bottom: 15px; +`; diff --git a/src/components/panes/MethodPane/MethodPane.tsx b/src/components/panes/MethodPane/MethodPane.tsx index 2280267..c724170 100644 --- a/src/components/panes/MethodPane/MethodPane.tsx +++ b/src/components/panes/MethodPane/MethodPane.tsx @@ -7,7 +7,12 @@ import { import { State } from "../../../store/state"; import { nextPane } from "../../../store/layout/actions"; import { Pane } from "../Panes.style"; -import { MethodWrapper, MethodButton, InfoText } from "./MethodPane.style"; +import { + MethodWrapper, + MethodButton, + InfoText, + RecurringSelectWrapper, +} from "./MethodPane.style"; import { RichSelect } from "../../shared/RichSelect/RichSelect"; import { RichSelectOption } from "../../shared/RichSelect/RichSelectOption"; import { PaymentMethod, RecurringDonation } from "../../../types/Enums"; @@ -27,20 +32,22 @@ export const MethodPane: React.FC = () => { Kostnadene angitt dekkes av oss, slik at 100% av din donasjon kommer frem. - dispatch(setRecurring(value))} - > - - - + + dispatch(setRecurring(value))} + > + + + + - {props.text ? props.text : "Neste"} - - ); -} - -export function PrevButton() { - const currentPaneNumber = useSelector( - (state: State) => state.layout.paneNumber - ); - const isCustomShare = useSelector((state: State) => state.layout.shareType); - const dispatch = useDispatch(); - - function goBack() { - if (!isCustomShare && currentPaneNumber === 4) { - dispatch(setPaneNumber(currentPaneNumber - 2)); - } else { - dispatch(setPaneNumber(currentPaneNumber - 1)); - } - } - - return ( - - Tilbake - - ); -} - -export function OrangeButton(props: any) { - return ( - - {props.text} - - ); -} - -export function NavButton(props: any) { - return ( - - {props.text} - - ); -} - -// END REMOVE diff --git a/src/components/shared/Input/TextInput.style.tsx b/src/components/shared/Input/TextInput.style.tsx new file mode 100644 index 0000000..de32223 --- /dev/null +++ b/src/components/shared/Input/TextInput.style.tsx @@ -0,0 +1,93 @@ +import styled from "styled-components"; +import { gray18, orange15 } from "../../../config/colors"; + +export interface TextInputProps extends TextInputWrapperProps { + type: string; + name?: string; + placeholder?: string; + defaultValue?: string | number; + selectOnClick?: boolean; + innerRef?: React.Ref; +} + +export interface TextInputWrapperProps { + label?: string; + denomination?: string; +} + +export const TextInputWrapper = styled.div` + display: block; + margin-bottom: 5px; + font-size: 15px; + border: 1px solid ${gray18}; + border-radius: 5px; + box-sizing: border-box; + position: relative; + + &:before { + content: "${(props: TextInputWrapperProps) => props.label}"; + height: 100%; + position: absolute; + left: 12px; + top: 0; + color: black; + display: flex; + justify-content: center; + align-items: center; + font-weight: normal; + } + + ${(props: TextInputWrapperProps) => { + if (props.denomination) { + return ` + &:after { + content: "${props.denomination}"; + height: 100%; + position: absolute; + right: 12px; + top: 0; + color: ${gray18}; + display: flex; + justify-content: center; + align-items: center; + font-weight: normal; + } + `; + } + return ""; + }} + + transition: box-shadow 180ms; + &:focus-within { + box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.3); + } +`; + +export const TextInputField = styled.input` + font-size: inherit; + padding: 20px; + ${(props: TextInputProps) => { + if (props.denomination) { + return ` + padding-right: 30px; + `; + } + return ""; + }} + text-align: ${(props: TextInputProps) => (props.label ? "right" : "left")}; + border: none; + box-sizing: border-box; + width: 100%; + background: transparent; + box-shadow: none; + position: relative; + z-index: 2; + border-radius: 5px; + display: block; + + transition: box-shadow 180ms; + &:focus { + outline: none; + box-shadow: 0px 0px 0px 1.5px ${orange15}; + } +`; diff --git a/src/components/shared/Input/TextInput.tsx b/src/components/shared/Input/TextInput.tsx new file mode 100644 index 0000000..cee0860 --- /dev/null +++ b/src/components/shared/Input/TextInput.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { + TextInputField, + TextInputProps, + TextInputWrapper, +} from "./TextInput.style"; + +export const TextInput: React.FC = ({ + label, + denomination, + name, + type, + placeholder, + defaultValue, + innerRef, + selectOnClick, +}) => { + return ( + + { + if (selectOnClick) e.currentTarget.select(); + }} + /> + + ); +}; diff --git a/src/components/shared/RichSelect/RichSelect.style.tsx b/src/components/shared/RichSelect/RichSelect.style.tsx index f92c56e..89c0001 100644 --- a/src/components/shared/RichSelect/RichSelect.style.tsx +++ b/src/components/shared/RichSelect/RichSelect.style.tsx @@ -6,7 +6,7 @@ export const RichSelectWrapper = styled.div` padding: 0 12px; border: 1px solid ${gray18}; - div:last-child { + & > div:last-child { border-bottom: none; } `; diff --git a/src/components/shared/RichSelect/RichSelectOption.style.tsx b/src/components/shared/RichSelect/RichSelectOption.style.tsx index 49b580d..59f800b 100644 --- a/src/components/shared/RichSelect/RichSelectOption.style.tsx +++ b/src/components/shared/RichSelect/RichSelectOption.style.tsx @@ -46,8 +46,6 @@ export const HeaderWrapper = styled.div` margin-left: 10px; `; -// ${(props: RadioBallProps) => props.selected ? '10px' : '22px'}; - export const RadioBall = styled.div` width: 24px; height: 24px; @@ -76,7 +74,7 @@ interface RadioBallProps { export const Content = styled.div` height: ${(props: ContentProps) => (props.selected ? "auto" : "0px")}; - overflow: hidden; + overflow: ${(props: ContentProps) => (props.selected ? "visible" : "hidden")}; box-sizing: border-box; `; diff --git a/src/store/donation/actions.ts b/src/store/donation/actions.ts index d0efd72..b13be65 100644 --- a/src/store/donation/actions.ts +++ b/src/store/donation/actions.ts @@ -10,9 +10,11 @@ import { SET_DONOR_ID, SET_KID, SET_PAYMENT_PROVIDER_URL, + SELECT_CUSTOM_SHARE, + SET_SHARE_TYPE, } from "./types"; -import { PaymentMethod, RecurringDonation } from "../../types/Enums"; -import { Shares } from "../../types/Temp"; +import { PaymentMethod, RecurringDonation, ShareType } from "../../types/Enums"; +import { OrganizationShare } from "../../types/Temp"; const actionCreator = actionCreatorFactory(); @@ -55,7 +57,7 @@ export function submitDonorInfo( }; } -export function setShares(shares: Shares): DonationActionTypes { +export function setShares(shares: OrganizationShare[]): DonationActionTypes { return { type: SET_SHARES, payload: { @@ -111,6 +113,24 @@ export function setPaymentProviderURL(url: string): DonationActionTypes { }; } +export function selectCustomShare(customShare: boolean): DonationActionTypes { + return { + type: SELECT_CUSTOM_SHARE, + payload: { + customShare, + }, + }; +} + +export function setShareType(shareType: ShareType): DonationActionTypes { + return { + type: SET_SHARE_TYPE, + payload: { + shareType, + }, + }; +} + /** * TODO: Find a place this can live */ diff --git a/src/store/donation/reducer.ts b/src/store/donation/reducer.ts index eed7f18..f2bc99c 100644 --- a/src/store/donation/reducer.ts +++ b/src/store/donation/reducer.ts @@ -1,6 +1,8 @@ import { Reducer } from "redux"; import { isType } from "typescript-fsa"; -import { RecurringDonation } from "../../types/Enums"; +import { RecurringDonation, ShareType } from "../../types/Enums"; +import { OrganizationShare } from "../../types/Temp"; +import { fetchOrganizationsAction } from "../layout/actions"; import { Donation } from "../state"; import { registerDonationAction } from "./actions"; import { @@ -14,16 +16,19 @@ import { SET_KID, SET_DONOR_ID, SET_PAYMENT_PROVIDER_URL, + SET_SHARE_TYPE, + SELECT_CUSTOM_SHARE, } from "./types"; const initialState: Donation = { recurring: RecurringDonation.RECURRING, - sum: 0, + shareType: ShareType.STANDARD, donor: { taxDeduction: false, newsletter: false, }, isValid: true, + shares: [], }; /** @@ -38,8 +43,20 @@ export const donationReducer: Reducer = ( state: Donation = initialState, action: DonationActionTypes ) => { + if (isType(action, fetchOrganizationsAction.done)) { + state = { + ...state, + shares: action.payload.result.map( + (org): OrganizationShare => ({ + id: org.id, + share: org.standardShare, + }) + ), + }; + } + if (isType(action, registerDonationAction.done)) { - return { + state = { ...state, kid: action.payload.result.KID, paymentProviderURL: action.payload.result.paymentProviderUrl, @@ -52,11 +69,16 @@ export const donationReducer: Reducer = ( switch (action.type) { case SELECT_PAYMENT_METHOD: - return { ...state, method: action.payload.method }; + state = { ...state, method: action.payload.method }; + break; case SELECT_TAX_DEDUCTION: - return { ...state, taxDeduction: action.payload.taxDeduction }; + state = { + ...state, + donor: { ...state.donor, taxDeduction: action.payload.taxDeduction }, + }; + break; case SUBMIT_DONOR_INFO: - return { + state = { ...state, donor: { name: action.payload.name, @@ -66,22 +88,45 @@ export const donationReducer: Reducer = ( newsletter: action.payload.newsletter, }, }; + break; case SET_SHARES: - return { ...state, shares: { ...action.payload.shares } }; + state = { ...state, shares: action.payload.shares }; + break; case SET_SUM: - return { ...state, sum: action.payload.sum }; + state = { ...state, sum: action.payload.sum }; + break; case SET_RECURRING: - return { ...state, recurring: action.payload.recurring }; + state = { ...state, recurring: action.payload.recurring }; + break; case SET_KID: - return { ...state, kid: action.payload.kid }; + state = { ...state, kid: action.payload.kid }; + break; case SET_DONOR_ID: - return { + state = { ...state, donor: { ...state.donor, donorID: action.payload.donorID }, }; + break; case SET_PAYMENT_PROVIDER_URL: - return { ...state, paymentProviderURL: action.payload.url }; + state = { ...state, paymentProviderURL: action.payload.url }; + break; + case SET_SHARE_TYPE: + state = { ...state, shareType: action.payload.shareType }; + break; + case SELECT_CUSTOM_SHARE: + state = { ...state, shareType: ShareType.CUSTOM }; + break; default: - return state; } + + /** + * Validate donation + */ + if ( + state.shareType === ShareType.CUSTOM && + state.shares.reduce((acc, curr) => acc + curr.share, 0) !== 100 + ) + return { ...state, isValid: false }; + + return { ...state, isValid: true }; }; diff --git a/src/store/donation/saga.ts b/src/store/donation/saga.ts index a428367..afea7da 100644 --- a/src/store/donation/saga.ts +++ b/src/store/donation/saga.ts @@ -2,6 +2,7 @@ import { SagaIterator } from "redux-saga"; import { call, put, select } from "redux-saga/effects"; import { Action } from "typescript-fsa"; import { API_URL } from "../../config/api"; +import { ShareType } from "../../types/Enums"; import { IServerResponse } from "../../types/Temp"; import { nextPane, setAnsweredReferral, setLoading } from "../layout/actions"; import { Donation, State } from "../state"; @@ -13,13 +14,35 @@ export function* registerDonation( yield put(setLoading(true)); try { const donation: Donation = yield select((state: State) => state.donation); + + /** + * TODO: Ugly solution, in need of refactor + */ + let data; + if (donation.shareType === ShareType.STANDARD) { + data = { + donor: donation.donor, + method: donation.method, + recurring: donation.recurring, + sum: donation.sum, + }; + } else { + data = { + donor: donation.donor, + method: donation.method, + recurring: donation.recurring, + sum: donation.sum, + shares: donation.shares, + }; + } + const request = yield call(fetch, `${API_URL}/donations/register`, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, - body: JSON.stringify(donation), + body: JSON.stringify(data), }); const result: IServerResponse = yield call( request.json.bind(request) diff --git a/src/store/donation/types.ts b/src/store/donation/types.ts index 7f1cffe..da6c209 100644 --- a/src/store/donation/types.ts +++ b/src/store/donation/types.ts @@ -1,5 +1,5 @@ -import { PaymentMethod, RecurringDonation } from "../../types/Enums"; -import { Shares } from "../../types/Temp"; +import { PaymentMethod, RecurringDonation, ShareType } from "../../types/Enums"; +import { OrganizationShare } from "../../types/Temp"; export const SELECT_PAYMENT_METHOD = "SELECT_PAYMENT_METHOD"; export const SELECT_TAX_DEDUCTION = "SELECT_TAX_DEDUCTION"; @@ -10,6 +10,8 @@ export const SET_RECURRING = "SET_RECURRING"; export const SET_KID = "SET_KID"; export const SET_DONOR_ID = "SET_DONOR_ID"; export const SET_PAYMENT_PROVIDER_URL = "SET_PAYMENT_PROVIDER_URL"; +export const SELECT_CUSTOM_SHARE = "SELECT_CUSTOM_SHARE"; +export const SET_SHARE_TYPE = "SET_SHARE_TYPE"; interface SelectPaymentMethod { type: typeof SELECT_PAYMENT_METHOD; @@ -39,7 +41,7 @@ interface SubmitDonorInfo { interface SetShares { type: typeof SET_SHARES; payload: { - shares: Shares; + shares: OrganizationShare[]; }; } @@ -78,6 +80,20 @@ interface SetPaymentProviderURL { }; } +interface SelectCustomShare { + type: typeof SELECT_CUSTOM_SHARE; + payload: { + customShare: boolean; + }; +} + +interface SetShareType { + type: typeof SET_SHARE_TYPE; + payload: { + shareType: ShareType; + }; +} + export type DonationActionTypes = | SelectPaymentMethod | SelectTaxDeduction @@ -87,4 +103,6 @@ export type DonationActionTypes = | SetRecurring | SetKID | SetDonorID - | SetPaymentProviderURL; + | SetPaymentProviderURL + | SelectCustomShare + | SetShareType; diff --git a/src/store/layout/actions.ts b/src/store/layout/actions.ts index d185148..abe8330 100644 --- a/src/store/layout/actions.ts +++ b/src/store/layout/actions.ts @@ -1,38 +1,17 @@ import actionCreatorFactory from "typescript-fsa"; -import { ShareType } from "../../types/Enums"; import { Organization } from "../../types/Organization"; import { DECREMENT_CURRENT_PANE, INCREMENT_CURRENT_PANE, LayoutActionTypes, - SELECT_CUSTOM_SHARE, SELECT_PRIVACY_POLICY, SET_ANSWERED_REFERRAL, SET_LOADING, SET_PANE_NUMBER, - SET_SHARE_TYPE, } from "./types"; const actionCreator = actionCreatorFactory(); -export function selectCustomShare(customShare: boolean): LayoutActionTypes { - return { - type: SELECT_CUSTOM_SHARE, - payload: { - customShare, - }, - }; -} - -export function setShareType(shareType: ShareType): LayoutActionTypes { - return { - type: SET_SHARE_TYPE, - payload: { - shareType, - }, - }; -} - export function selectPrivacyPolicy(privacyPolicy: boolean): LayoutActionTypes { return { type: SELECT_PRIVACY_POLICY, @@ -83,6 +62,6 @@ export function setLoading(loading: boolean): LayoutActionTypes { export const fetchOrganizationsAction = actionCreator.async< undefined, - [Organization], + Organization[], Error >("FETCH_ORGANIZATIONS"); diff --git a/src/store/layout/reducer.ts b/src/store/layout/reducer.ts index 3e21059..ebeb483 100644 --- a/src/store/layout/reducer.ts +++ b/src/store/layout/reducer.ts @@ -1,24 +1,20 @@ import { Reducer } from "redux"; import { isType } from "typescript-fsa"; -import { ShareType } from "../../types/Enums"; import { Layout } from "../state"; import { fetchOrganizationsAction } from "./actions"; import { SET_PANE_NUMBER, LayoutActionTypes, - SELECT_CUSTOM_SHARE, SELECT_PRIVACY_POLICY, SET_ANSWERED_REFERRAL, SET_HEIGHT, INCREMENT_CURRENT_PANE, DECREMENT_CURRENT_PANE, - SET_SHARE_TYPE, SET_LOADING, } from "./types"; const initialState: Layout = { privacyPolicy: false, - shareType: ShareType.STANDARD, paneNumber: 0, height: 512, loading: false, @@ -44,10 +40,6 @@ export const layoutReducer: Reducer = ( } switch (action.type) { - case SELECT_CUSTOM_SHARE: - return { ...state, shareType: ShareType.CUSTOM }; - case SET_SHARE_TYPE: - return { ...state, shareType: action.payload.shareType }; case SELECT_PRIVACY_POLICY: return { ...state, privacyPolicy: action.payload.privacyPolicy }; case SET_PANE_NUMBER: diff --git a/src/store/layout/saga.ts b/src/store/layout/saga.ts index 1a2e7f7..e5696b4 100644 --- a/src/store/layout/saga.ts +++ b/src/store/layout/saga.ts @@ -11,7 +11,7 @@ export function* fetchOrganizations( ): SagaIterator { try { const request = yield call(fetch, `${API_URL}/organizations/active/`); - const result: IServerResponse<[Organization]> = yield call( + const result: IServerResponse = yield call( request.json.bind(request) ); if (result.status !== 200) throw new Error(result.content as string); @@ -19,7 +19,7 @@ export function* fetchOrganizations( yield put( fetchOrganizationsAction.done({ params: action.payload, - result: result.content as [Organization], + result: result.content as Organization[], }) ); } catch (ex) { diff --git a/src/store/layout/types.ts b/src/store/layout/types.ts index 186ee16..87e85bb 100644 --- a/src/store/layout/types.ts +++ b/src/store/layout/types.ts @@ -1,7 +1,3 @@ -import { ShareType } from "../../types/Enums"; - -export const SELECT_CUSTOM_SHARE = "SELECT_CUSTOM_SHARE"; -export const SET_SHARE_TYPE = "SET_SHARE_TYPE"; export const SELECT_PRIVACY_POLICY = "SELECT_PRIVACY_POLICY"; export const SET_PANE_NUMBER = "SET_PANE_NUMBER"; export const INCREMENT_CURRENT_PANE = "INCREMENT_CURRENT_PANE"; @@ -10,20 +6,6 @@ export const SET_ANSWERED_REFERRAL = "SET_ANSWERRED_REFERRAL"; export const SET_HEIGHT = "SET_HEIGHT"; export const SET_LOADING = "SET_LOADING"; -interface SelectCustomShare { - type: typeof SELECT_CUSTOM_SHARE; - payload: { - customShare: boolean; - }; -} - -interface SetShareType { - type: typeof SET_SHARE_TYPE; - payload: { - shareType: ShareType; - }; -} - interface SelectPrivacyPolicy { type: typeof SELECT_PRIVACY_POLICY; payload: { @@ -66,12 +48,10 @@ interface SetLoading { } export type LayoutActionTypes = - | SelectCustomShare | SelectPrivacyPolicy | SetPaneNumber | IncrementCurrentPane | DecrementCurrentPane | SetAnsweredReferral | SetHeight - | SetShareType | SetLoading; diff --git a/src/store/state.ts b/src/store/state.ts index 87e9f13..8b726fe 100644 --- a/src/store/state.ts +++ b/src/store/state.ts @@ -1,6 +1,6 @@ import { PaymentMethod, RecurringDonation, ShareType } from "../types/Enums"; import { Organization } from "../types/Organization"; -import { ReferralType, Shares } from "../types/Temp"; +import { OrganizationShare, ReferralType } from "../types/Temp"; export interface State { donation: Donation; @@ -11,20 +11,20 @@ export interface State { export interface Layout { privacyPolicy: boolean; - shareType: ShareType; paneNumber: number; answeredReferral?: boolean; height: number; loading: boolean; - organizations?: [Organization]; + organizations?: Organization[]; } export interface DonationInput { method?: PaymentMethod; - sum: number; + sum?: number; + shareType: ShareType; recurring: RecurringDonation; donor?: Donor; - shares?: Shares; + shares: OrganizationShare[]; } export interface Donation extends DonationInput { diff --git a/src/types/Temp.ts b/src/types/Temp.ts index 518e6db..9db5aab 100644 --- a/src/types/Temp.ts +++ b/src/types/Temp.ts @@ -4,7 +4,7 @@ export enum DonorType { } export interface Shares { - [key: string]: number; + [id: number]: number; } export interface IServerResponse { @@ -15,7 +15,6 @@ export interface IServerResponse { export interface OrganizationShare { id: number; share: number; - name: string; } export interface ReferralData {