diff --git a/client/src/client-only-routes/show-certification.tsx b/client/src/client-only-routes/show-certification.tsx index d96e17f4c1..c91126a6d3 100644 --- a/client/src/client-only-routes/show-certification.tsx +++ b/client/src/client-only-routes/show-certification.tsx @@ -48,7 +48,7 @@ interface IShowCertificationProps { cert: CertType; certDashedName: string; certSlug: string; - createFlashMessage: (payload: typeof standardErrorMessage) => void; + createFlashMessage: typeof createFlashMessage; executeGA: (payload: Record) => void; fetchProfileForUser: (username: string) => void; fetchState: { diff --git a/client/src/client-only-routes/show-settings.tsx b/client/src/client-only-routes/show-settings.tsx index 76933b23b9..44e12fdc2b 100644 --- a/client/src/client-only-routes/show-settings.tsx +++ b/client/src/client-only-routes/show-settings.tsx @@ -29,7 +29,7 @@ const { apiLocation } = envData; // TODO: update types for actions interface IShowSettingsProps { - createFlashMessage: (paylaod: string[]) => void; + createFlashMessage: typeof createFlashMessage; isSignedIn: boolean; navigate: (location: string) => void; showLoading: boolean; diff --git a/client/src/components/Flash/index.tsx b/client/src/components/Flash/index.tsx index e27cd0f28b..6accd82936 100644 --- a/client/src/components/Flash/index.tsx +++ b/client/src/components/Flash/index.tsx @@ -1,23 +1,19 @@ import { Alert } from '@freecodecamp/react-bootstrap'; -import PropTypes from 'prop-types'; import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; +import { FlashState } from '../../redux/types'; +import { removeFlashMessage } from './redux'; import './flash.css'; type FlashProps = { - flashMessage: { - type: string; - message: string; - id: string; - variables: Record; - }; - onClose: () => void; + flashMessage: FlashState['message']; + removeFlashMessage: typeof removeFlashMessage; }; -function Flash({ flashMessage, onClose }: FlashProps): JSX.Element { - const { type, message, id, variables = {} } = flashMessage; +function Flash({ flashMessage, removeFlashMessage }: FlashProps): JSX.Element { + const { type, message, id, variables } = flashMessage; const { t } = useTranslation(); const [flashMessageHeight, setFlashMessageHeight] = useState(0); @@ -33,7 +29,7 @@ function Flash({ flashMessage, onClose }: FlashProps): JSX.Element { function handleClose() { document.documentElement.style.setProperty('--flash-message-height', '0px'); - onClose(); + removeFlashMessage(); } return ( @@ -62,14 +58,5 @@ function Flash({ flashMessage, onClose }: FlashProps): JSX.Element { } Flash.displayName = 'FlashMessages'; -Flash.propTypes = { - flashMessage: PropTypes.shape({ - id: PropTypes.string, - type: PropTypes.string, - message: PropTypes.string, - variables: PropTypes.object - }), - onClose: PropTypes.func.isRequired -}; export default Flash; diff --git a/client/src/components/Flash/redux/index.ts b/client/src/components/Flash/redux/index.ts index 46059692a8..66ea341587 100644 --- a/client/src/components/Flash/redux/index.ts +++ b/client/src/components/Flash/redux/index.ts @@ -1,41 +1,66 @@ import { nanoid } from 'nanoid'; -import { createAction, handleActions } from 'redux-actions'; +import { FlashState, State } from '../../../redux/types'; -import { createTypes } from '../../../utils/create-types'; - -export const ns = 'flash'; +export const FlashApp = 'flash'; const initialState = { - message: {} + message: { + id: '', + type: '', + message: '' + } }; -export const types = createTypes( - ['createFlashMessage', 'removeFlashMessage'], - ns -); - export const sagas = []; -export const createFlashMessage = createAction( - types.createFlashMessage, - (msg: string[]) => ({ id: nanoid(), ...msg }) -); -export const removeFlashMessage = createAction(types.removeFlashMessage); +export const flashMessageSelector = (state: State): FlashState['message'] => + state[FlashApp].message; -// TODO: Once state is typed, add here, remove disable. -// eslint-disable-next-line -export const flashMessageSelector = (state: any): string => state[ns].message; +// ACTION DEFINITIONS -export const reducer = handleActions( - { - [types.createFlashMessage]: (state, { payload }) => ({ - ...state, - message: payload - }), - [types.removeFlashMessage]: state => ({ - ...state, - message: {} - }) - }, - initialState -); +enum FlashActionTypes { + createFlashMessage = 'createFlashMessage', + removeFlashMessage = 'removeFlashMessage' +} + +export type FlashMessageArg = { + type: string; + message: string; + variables?: Record; +}; + +export const createFlashMessage = ( + flash: FlashMessageArg +): ReducerPayload => ({ + type: FlashActionTypes.createFlashMessage, + payload: { ...flash, id: nanoid() } +}); + +export const removeFlashMessage = + (): ReducerPayload => ({ + type: FlashActionTypes.removeFlashMessage + }); + +// REDUCER +type ReducerBase = { type: T }; +type ReducerPayload = + T extends FlashActionTypes.createFlashMessage + ? ReducerBase & { + payload: FlashState['message']; + } + : ReducerBase; + +// Does reducer return FlashState or AppState (whole app)? +export const reducer = ( + state: FlashState = initialState, + action: ReducerPayload +): FlashState => { + switch (action.type) { + case FlashActionTypes.createFlashMessage: + return { ...state, message: action.payload }; + case FlashActionTypes.removeFlashMessage: + return { ...state, message: initialState.message }; + default: + return state; + } +}; diff --git a/client/src/components/layouts/Default.js b/client/src/components/layouts/Default.js index 94631471ae..15779a8298 100644 --- a/client/src/components/layouts/Default.js +++ b/client/src/components/layouts/Default.js @@ -217,7 +217,10 @@ class DefaultLayout extends Component { isSignedIn={isSignedIn} /> {hasMessage && flashMessage ? ( - + ) : null} {children} diff --git a/client/src/redux/index.js b/client/src/redux/index.js index 4f67d97aaf..25ef52d43c 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -5,7 +5,7 @@ import store from 'store'; import { actionTypes as challengeTypes } from '../templates/Challenges/redux/action-types'; import { CURRENT_CHALLENGE_KEY } from '../templates/Challenges/redux/current-challenge-saga'; import { createAcceptTermsSaga } from './accept-terms-saga'; -import { actionTypes, ns } from './action-types'; +import { actionTypes } from './action-types'; import { createAppMountSaga } from './app-mount-saga'; import { createDonationSaga } from './donation-saga'; import failedUpdatesEpic from './failed-updates-epic'; @@ -19,7 +19,7 @@ import { actionTypes as settingsTypes } from './settings/action-types'; import { createShowCertSaga } from './show-cert-saga'; import updateCompleteEpic from './update-complete-epic'; -export { ns }; +export const MainApp = 'app'; export const defaultFetchState = { pending: true, @@ -172,8 +172,9 @@ export const updateCurrentChallengeId = createAction( export const completedChallengesSelector = state => userSelector(state).completedChallenges || []; -export const completionCountSelector = state => state[ns].completionCount; -export const currentChallengeIdSelector = state => state[ns].currentChallengeId; +export const completionCountSelector = state => state[MainApp].completionCount; +export const currentChallengeIdSelector = state => + state[MainApp].currentChallengeId; export const stepsToClaimSelector = state => { const user = userSelector(state); const currentCerts = certificatesByNameSelector(user.username)( @@ -188,21 +189,24 @@ export const stepsToClaimSelector = state => { }; }; export const isDonatingSelector = state => userSelector(state).isDonating; -export const isOnlineSelector = state => state[ns].isOnline; -export const isServerOnlineSelector = state => state[ns].isServerOnline; -export const isSignedInSelector = state => !!state[ns].appUsername; -export const isDonationModalOpenSelector = state => state[ns].showDonationModal; +export const isOnlineSelector = state => state[MainApp].isOnline; +export const isServerOnlineSelector = state => state[MainApp].isServerOnline; +export const isSignedInSelector = state => !!state[MainApp].appUsername; +export const isDonationModalOpenSelector = state => + state[MainApp].showDonationModal; export const recentlyClaimedBlockSelector = state => - state[ns].recentlyClaimedBlock; -export const donationFormStateSelector = state => state[ns].donationFormState; + state[MainApp].recentlyClaimedBlock; +export const donationFormStateSelector = state => + state[MainApp].donationFormState; export const signInLoadingSelector = state => userFetchStateSelector(state).pending; -export const showCertSelector = state => state[ns].showCert; -export const showCertFetchStateSelector = state => state[ns].showCertFetchState; +export const showCertSelector = state => state[MainApp].showCert; +export const showCertFetchStateSelector = state => + state[MainApp].showCertFetchState; export const shouldRequestDonationSelector = state => { const completedChallenges = completedChallengesSelector(state); const completionCount = completionCountSelector(state); - const canRequestProgressDonation = state[ns].canRequestProgressDonation; + const canRequestProgressDonation = state[MainApp].canRequestProgressDonation; const isDonating = isDonatingSelector(state); const recentlyClaimedBlock = recentlyClaimedBlockSelector(state); @@ -226,7 +230,7 @@ export const shouldRequestDonationSelector = state => { }; export const userByNameSelector = username => state => { - const { user } = state[ns]; + const { user } = state[MainApp]; // return initial state empty user empty object instead of empty // object litteral to prevent components from re-rendering unnecessarily return user[username] ?? initialState.user; @@ -357,17 +361,17 @@ export const certificatesByNameSelector = username => state => { }; }; -export const userFetchStateSelector = state => state[ns].userFetchState; +export const userFetchStateSelector = state => state[MainApp].userFetchState; export const userProfileFetchStateSelector = state => - state[ns].userProfileFetchState; -export const usernameSelector = state => state[ns].appUsername; + state[MainApp].userProfileFetchState; +export const usernameSelector = state => state[MainApp].appUsername; export const userSelector = state => { const username = usernameSelector(state); - return state[ns].user[username] || {}; + return state[MainApp].user[username] || {}; }; -export const sessionMetaSelector = state => state[ns].sessionMeta; +export const sessionMetaSelector = state => state[MainApp].sessionMeta; function spreadThePayloadOnUser(state, payload) { return { diff --git a/client/src/redux/rootReducer.js b/client/src/redux/rootReducer.js index 587b52e66b..c914b2da87 100644 --- a/client/src/redux/rootReducer.js +++ b/client/src/redux/rootReducer.js @@ -2,7 +2,7 @@ import { combineReducers } from 'redux'; import { reducer as flash, - ns as flashNameSpace + FlashApp as flashNameSpace } from '../components/Flash/redux'; import { reducer as search, @@ -17,7 +17,7 @@ import { ns as curriculumMapNameSpace } from '../templates/Introduction/redux'; import { reducer as settings, ns as settingsNameSpace } from './settings'; -import { reducer as app, ns as appNameSpace } from './'; +import { reducer as app, MainApp as appNameSpace } from './'; export default combineReducers({ [appNameSpace]: app, diff --git a/client/src/redux/types.ts b/client/src/redux/types.ts new file mode 100644 index 0000000000..999362df8c --- /dev/null +++ b/client/src/redux/types.ts @@ -0,0 +1,56 @@ +import { FlashApp, FlashMessageArg } from '../components/Flash/redux'; +import { MainApp } from '.'; + +export interface State { + [FlashApp]: FlashState; + [MainApp]: { + appUsername: string; + recentlyClaimedBlock: null | string; + canRequestProgressDonation: boolean; + completionCount: number; + currentChallengId: string; + showCert: Record; + showCertFetchState: DefaultFetchState; + user: Record; + userFetchState: DefaultFetchState; + userProfileFetchState: DefaultFetchState; + sessionMeta: { + activeDonations: number; + }; + showDonationModal: boolean; + isOnline: boolean; + donationFormState: DefaultDonationFormState; + }; +} + +export interface FlashState { + message: { id: string } & FlashMessageArg; +} + +export interface DefaultFetchState { + pending: boolean; + complete: boolean; + errored: boolean; + error: null | string; +} + +export interface DefaultDonationFormState { + redirecting: boolean; + processing: boolean; + success: boolean; + error: null | string; +} + +export const defaultFetchState = { + pending: true, + complete: false, + errored: false, + error: null +}; + +export const defaultDonationFormState = { + redirecting: false, + processing: false, + success: false, + error: '' +};