diff --git a/.eslintrc.json b/.eslintrc.json index 4a87d628a73..86091343770 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -36,6 +36,7 @@ "import/no-named-as-default": "off", "import/no-named-as-default-member": "off", "import/order": "error", + "import/no-cycle": [2, { "maxDepth": 2 }], "react/prop-types": "off", "no-only-tests/no-only-tests": "error" }, diff --git a/client/src/client-only-routes/show-certification.tsx b/client/src/client-only-routes/show-certification.tsx index a398f5be85c..a46b0886c34 100644 --- a/client/src/client-only-routes/show-certification.tsx +++ b/client/src/client-only-routes/show-certification.tsx @@ -15,17 +15,15 @@ import { createFlashMessage } from '../components/Flash/redux'; import { Loader, Spacer } from '../components/helpers'; import RedirectHome from '../components/redirect-home'; import { Themes } from '../components/settings/theme'; +import { showCert, executeGA, fetchProfileForUser } from '../redux/actions'; import { showCertSelector, showCertFetchStateSelector, - showCert, userFetchStateSelector, - usernameSelector, isDonatingSelector, - executeGA, userByNameSelector, - fetchProfileForUser -} from '../redux'; + usernameSelector +} from '../redux/selectors'; import { UserFetchState, User } from '../redux/prop-types'; import { certMap } from '../resources/cert-and-project-map'; import certificateMissingMessage from '../utils/certificate-missing-message'; diff --git a/client/src/client-only-routes/show-profile-or-four-oh-four.tsx b/client/src/client-only-routes/show-profile-or-four-oh-four.tsx index f11e8679dcc..0781860c65a 100644 --- a/client/src/client-only-routes/show-profile-or-four-oh-four.tsx +++ b/client/src/client-only-routes/show-profile-or-four-oh-four.tsx @@ -6,12 +6,12 @@ import { isBrowser } from '../../utils/index'; import FourOhFour from '../components/FourOhFour'; import Loader from '../components/helpers/loader'; import Profile from '../components/profile/profile'; +import { fetchProfileForUser } from '../redux/actions'; import { + usernameSelector, userByNameSelector, - userProfileFetchStateSelector, - fetchProfileForUser, - usernameSelector -} from '../redux'; + userProfileFetchStateSelector +} from '../redux/selectors'; import { User } from '../redux/prop-types'; interface ShowProfileOrFourOhFourProps { diff --git a/client/src/client-only-routes/show-project-links.tsx b/client/src/client-only-routes/show-project-links.tsx index 9487a8c64f6..cbe56ea2a05 100644 --- a/client/src/client-only-routes/show-project-links.tsx +++ b/client/src/client-only-routes/show-project-links.tsx @@ -14,7 +14,7 @@ import { import { SolutionDisplayWidget } from '../components/solution-display-widget'; import ProjectPreviewModal from '../templates/Challenges/components/project-preview-modal'; -import { openModal } from '../templates/Challenges/redux'; +import { openModal } from '../templates/Challenges/redux/actions'; import '../components/layouts/project-links.css'; import { regeneratePathAndHistory } from '../../../utils/polyvinyl'; diff --git a/client/src/client-only-routes/show-settings.tsx b/client/src/client-only-routes/show-settings.tsx index 28086a98e55..f8452f5541e 100644 --- a/client/src/client-only-routes/show-settings.tsx +++ b/client/src/client-only-routes/show-settings.tsx @@ -18,13 +18,13 @@ import Portfolio from '../components/settings/portfolio'; import Privacy from '../components/settings/privacy'; import { Themes } from '../components/settings/theme'; import UserToken from '../components/settings/user-token'; +import { hardGoTo as navigate } from '../redux/actions'; import { signInLoadingSelector, userSelector, isSignedInSelector, - hardGoTo as navigate, userTokenSelector -} from '../redux'; +} from '../redux/selectors'; import { User } from '../redux/prop-types'; import { submitNewAbout, @@ -36,7 +36,7 @@ import { updateMyTheme, updateMyKeyboardShortcuts, verifyCert -} from '../redux/settings'; +} from '../redux/settings/actions'; const { apiLocation } = envData; diff --git a/client/src/client-only-routes/show-user.tsx b/client/src/client-only-routes/show-user.tsx index 8231fbdc808..7379587ecab 100644 --- a/client/src/client-only-routes/show-user.tsx +++ b/client/src/client-only-routes/show-user.tsx @@ -16,12 +16,12 @@ import { createSelector } from 'reselect'; import Login from '../components/Header/components/Login'; import { Spacer, Loader, FullWidthRow } from '../components/helpers'; +import { reportUser } from '../redux/actions'; import { - isSignedInSelector, userFetchStateSelector, - userSelector, - reportUser -} from '../redux'; + isSignedInSelector, + userSelector +} from '../redux/selectors'; import { UserFetchState } from '../redux/prop-types'; interface ShowUserProps { diff --git a/client/src/components/Donation/donate-form.tsx b/client/src/components/Donation/donate-form.tsx index 3735adb90a8..4c5a91307c5 100644 --- a/client/src/components/Donation/donate-form.tsx +++ b/client/src/components/Donation/donate-form.tsx @@ -13,19 +13,21 @@ import { defaultDonation, modalDefaultDonation } from '../../../../config/donation-settings'; +import { defaultDonationFormState } from '../../redux'; +import { + addDonation, + updateDonationFormState, + postChargeStripe, + postChargeStripeCard +} from '../../redux/actions'; import { isSignedInSelector, + userSelector, isDonatingSelector, signInLoadingSelector, donationFormStateSelector, - addDonation, - updateDonationFormState, - defaultDonationFormState, - userSelector, - postChargeStripe, - postChargeStripeCard, isVariantASelector -} from '../../redux'; +} from '../../redux/selectors'; import Spacer from '../helpers/spacer'; import { Themes } from '../settings/theme'; import DonateCompletion from './donate-completion'; diff --git a/client/src/components/Donation/donation-modal.tsx b/client/src/components/Donation/donation-modal.tsx index 1829e7f4352..d35b833e2ef 100644 --- a/client/src/components/Donation/donation-modal.tsx +++ b/client/src/components/Donation/donation-modal.tsx @@ -10,12 +10,11 @@ import { modalDefaultDonation } from '../../../../config/donation-settings'; import Cup from '../../assets/icons/cup'; import Heart from '../../assets/icons/heart'; +import { closeDonationModal, executeGA } from '../../redux/actions'; import { - closeDonationModal, isDonationModalOpenSelector, - recentlyClaimedBlockSelector, - executeGA -} from '../../redux'; + recentlyClaimedBlockSelector +} from '../../redux/selectors'; import { isLocationSuperBlock } from '../../utils/path-parsers'; import { playTone } from '../../utils/tone'; import { Spacer } from '../helpers'; diff --git a/client/src/components/Donation/paypal-button.tsx b/client/src/components/Donation/paypal-button.tsx index afd4f567907..3102a066a72 100644 --- a/client/src/components/Donation/paypal-button.tsx +++ b/client/src/components/Donation/paypal-button.tsx @@ -8,7 +8,7 @@ import { defaultDonation } from '../../../../config/donation-settings'; import envData from '../../../../config/env.json'; -import { signInLoadingSelector, userSelector } from '../../redux'; +import { userSelector, signInLoadingSelector } from '../../redux/selectors'; import { Themes } from '../settings/theme'; import PayPalButtonScriptLoader from './paypal-button-script-loader'; diff --git a/client/src/components/Flash/redux/index.ts b/client/src/components/Flash/redux/index.ts index 40ed3123600..f84a3b04faa 100644 --- a/client/src/components/Flash/redux/index.ts +++ b/client/src/components/Flash/redux/index.ts @@ -1,12 +1,15 @@ import { nanoid } from 'nanoid'; -import { FlashState, State } from '../../../redux/types'; +import { + FlashState, + State, + FlashApp, + FlashMessageArg +} from '../../../redux/types'; import { playTone } from '../../../utils/tone'; import { Themes } from '../../settings/theme'; import { FlashMessages } from './flash-messages'; -export const FlashApp = 'flash'; - export const sagas = []; export const flashMessageSelector = (state: State): FlashState['message'] => @@ -19,12 +22,6 @@ enum FlashActionTypes { RemoveFlashMessage = 'removeFlashMessage' } -export type FlashMessageArg = { - type: string; - message: FlashMessages; - variables?: Record; -}; - const initialState = { message: { id: '', diff --git a/client/src/components/Header/components/Login.tsx b/client/src/components/Header/components/Login.tsx index 83c162bd159..85a1cd4b883 100644 --- a/client/src/components/Header/components/Login.tsx +++ b/client/src/components/Header/components/Login.tsx @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import envData from '../../../../../config/env.json'; -import { isSignedInSelector } from '../../../redux'; +import { isSignedInSelector } from '../../../redux/selectors'; const { apiLocation, homeLocation } = envData; diff --git a/client/src/components/Header/components/nav-links.tsx b/client/src/components/Header/components/nav-links.tsx index 9029d6e0954..14f2f54f569 100644 --- a/client/src/components/Header/components/nav-links.tsx +++ b/client/src/components/Header/components/nav-links.tsx @@ -18,8 +18,8 @@ import { LangCodes, hiddenLangs } from '../../../../../config/i18n/all-langs'; -import { hardGoTo as navigate } from '../../../redux'; -import { updateMyTheme } from '../../../redux/settings'; +import { hardGoTo as navigate } from '../../../redux/actions'; +import { updateMyTheme } from '../../../redux/settings/actions'; import createLanguageRedirect from '../../create-language-redirect'; import { Link } from '../../helpers'; import { Themes } from '../../settings/theme'; diff --git a/client/src/components/Header/components/user-state.tsx b/client/src/components/Header/components/user-state.tsx index 54e6d3339a0..91839d91c3d 100644 --- a/client/src/components/Header/components/user-state.tsx +++ b/client/src/components/Header/components/user-state.tsx @@ -9,7 +9,10 @@ import { connect } from 'react-redux'; import Spinner from 'react-spinkit'; import { createSelector } from 'reselect'; -import { isSignedInSelector, userFetchStateSelector } from '../../../redux'; +import { + isSignedInSelector, + userFetchStateSelector +} from '../../../redux/selectors'; import Login from './Login'; const mapStateToProps = createSelector( diff --git a/client/src/components/app-mount-notifier.tsx b/client/src/components/app-mount-notifier.tsx index 1b15347703d..c290a17aa95 100644 --- a/client/src/components/app-mount-notifier.tsx +++ b/client/src/components/app-mount-notifier.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; -import { appMount } from '../redux'; +import { appMount } from '../redux/actions'; interface AppMountNotifierProps { render: () => React.ReactNode; diff --git a/client/src/components/formHelpers/form-fields.tsx b/client/src/components/formHelpers/form-fields.tsx index 4b43881894d..ef308ae6794 100644 --- a/client/src/components/formHelpers/form-fields.tsx +++ b/client/src/components/formHelpers/form-fields.tsx @@ -11,7 +11,6 @@ import normalizeUrl from 'normalize-url'; import React from 'react'; import { Field } from 'react-final-form'; import { useTranslation } from 'react-i18next'; -import { FormOptions } from './form'; import { editorValidator, localhostValidator, @@ -20,6 +19,15 @@ import { httpValidator } from './form-validators'; +export type FormOptions = { + ignored?: string[]; + isEditorLinkAllowed?: boolean; + isLocalLinkAllowed?: boolean; + required?: string[]; + types?: { [key: string]: string }; + placeholders?: { [key: string]: string }; +}; + type FormFieldsProps = { formFields: { name: string; label: string }[]; options: FormOptions; diff --git a/client/src/components/formHelpers/form.tsx b/client/src/components/formHelpers/form.tsx index 3b9f800c2a1..64c174decee 100644 --- a/client/src/components/formHelpers/form.tsx +++ b/client/src/components/formHelpers/form.tsx @@ -1,24 +1,75 @@ import React, { FormEvent } from 'react'; import { Form } from 'react-final-form'; +import normalizeUrl from 'normalize-url'; import { - URLValues, - ValidatedValues, - FormFields, - BlockSaveButton, - BlockSaveWrapper, - formatUrlValues -} from '../formHelpers/index'; + localhostValidator, + editorValidator, + composeValidators, + fCCValidator, + httpValidator +} from './form-validators'; +import FormFields, { FormOptions } from './form-fields'; -export type FormOptions = { - ignored?: string[]; - isEditorLinkAllowed?: boolean; - isLocalLinkAllowed?: boolean; - required?: string[]; - types?: { [key: string]: string }; - placeholders?: { [key: string]: string }; +import { default as BlockSaveButton } from './block-save-button'; +import { default as BlockSaveWrapper } from './block-save-wrapper'; + +type URLValues = { + [key: string]: string; }; +type ValidationError = { + error: { message?: string }; + value: string; +}; + +export type ValidatedValues = { + values: URLValues; + errors: ValidationError[]; + invalidValues: (JSX.Element | null)[]; +}; + +const normalizeOptions = { + stripWWW: false +}; + +function formatUrlValues( + values: URLValues, + options: FormOptions +): ValidatedValues { + const { isEditorLinkAllowed, isLocalLinkAllowed, types } = options; + const validatedValues: ValidatedValues = { + values: {}, + errors: [], + invalidValues: [] + }; + const urlValues = Object.keys(values).reduce((result, key: string) => { + let value: string = values[key]; + const nullOrWarning: JSX.Element | null = composeValidators( + fCCValidator, + httpValidator, + isLocalLinkAllowed ? null : localhostValidator, + key === 'githubLink' || isEditorLinkAllowed ? null : editorValidator + )(value); + if (nullOrWarning) { + validatedValues.invalidValues.push(nullOrWarning); + } + if (value && types && types[key] === 'url') { + try { + value = normalizeUrl(value, normalizeOptions); + } catch (err: unknown) { + validatedValues.errors.push({ + error: err as { message?: string }, + value + }); + } + } + return { ...result, [key]: value }; + }, {}); + validatedValues.values = urlValues; + return validatedValues; +} + export type FormProps = { buttonText?: string; enableSubmit?: boolean; diff --git a/client/src/components/formHelpers/index.tsx b/client/src/components/formHelpers/index.tsx index 31931b3a100..d1f6f04d5e8 100644 --- a/client/src/components/formHelpers/index.tsx +++ b/client/src/components/formHelpers/index.tsx @@ -1,70 +1,5 @@ -import normalizeUrl from 'normalize-url'; -import { FormOptions } from './form'; -import { - localhostValidator, - editorValidator, - composeValidators, - fCCValidator, - httpValidator -} from './form-validators'; +import { default as Form, ValidatedValues } from './form'; export { default as BlockSaveButton } from './block-save-button'; export { default as BlockSaveWrapper } from './block-save-wrapper'; -export { default as Form } from './form'; -export { default as FormFields } from './form-fields'; - -const normalizeOptions = { - stripWWW: false -}; - -export type URLValues = { - [key: string]: string; -}; - -type ValidationError = { - error: { message?: string }; - value: string; -}; - -export type ValidatedValues = { - values: URLValues; - errors: ValidationError[]; - invalidValues: (JSX.Element | null)[]; -}; - -export function formatUrlValues( - values: URLValues, - options: FormOptions -): ValidatedValues { - const { isEditorLinkAllowed, isLocalLinkAllowed, types } = options; - const validatedValues: ValidatedValues = { - values: {}, - errors: [], - invalidValues: [] - }; - const urlValues = Object.keys(values).reduce((result, key: string) => { - let value: string = values[key]; - const nullOrWarning: JSX.Element | null = composeValidators( - fCCValidator, - httpValidator, - isLocalLinkAllowed ? null : localhostValidator, - key === 'githubLink' || isEditorLinkAllowed ? null : editorValidator - )(value); - if (nullOrWarning) { - validatedValues.invalidValues.push(nullOrWarning); - } - if (value && types && types[key] === 'url') { - try { - value = normalizeUrl(value, normalizeOptions); - } catch (err: unknown) { - validatedValues.errors.push({ - error: err as { message?: string }, - value - }); - } - } - return { ...result, [key]: value }; - }, {}); - validatedValues.values = urlValues; - return validatedValues; -} +export { Form, ValidatedValues }; diff --git a/client/src/components/helpers/avatar-renderer.tsx b/client/src/components/helpers/avatar-renderer.tsx index 79cce9bf606..4a9257df17e 100644 --- a/client/src/components/helpers/avatar-renderer.tsx +++ b/client/src/components/helpers/avatar-renderer.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import isURL from 'validator/lib/isURL'; import { defaultUserImage } from '../../../../config/misc'; import DefaultAvatar from '../../assets/icons/default-avatar'; -import { borderColorPicker } from '.'; +import borderColorPicker from './border-color-picker'; interface AvatarRendererProps { isDonating?: boolean; diff --git a/client/src/components/helpers/index.ts b/client/src/components/helpers/index.ts index b37f6d89ed3..d58dd56ae09 100644 --- a/client/src/components/helpers/index.ts +++ b/client/src/components/helpers/index.ts @@ -7,4 +7,3 @@ export { default as Spacer } from './spacer'; export { default as Link } from './link'; export { default as ImageLoader } from './image-loader'; export { default as AvatarRenderer } from './avatar-renderer'; -export { default as borderColorPicker } from './border-color-picker'; diff --git a/client/src/components/layouts/certification.tsx b/client/src/components/layouts/certification.tsx index 29aacc70d5b..74e13244146 100644 --- a/client/src/components/layouts/certification.tsx +++ b/client/src/components/layouts/certification.tsx @@ -3,7 +3,8 @@ import Helmet from 'react-helmet'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { fetchUser, isSignedInSelector, executeGA } from '../../redux'; +import { fetchUser, executeGA } from '../../redux/actions'; +import { isSignedInSelector } from '../../redux/selectors'; interface CertificationProps { children?: React.ReactNode; diff --git a/client/src/components/layouts/default.tsx b/client/src/components/layouts/default.tsx index 6802b377a02..c6588e8d60f 100644 --- a/client/src/components/layouts/default.tsx +++ b/client/src/components/layouts/default.tsx @@ -14,15 +14,17 @@ import robotoRegularURL from '../../../static/fonts/roboto-mono/RobotoMono-Regul import { isBrowser } from '../../../utils'; import { fetchUser, - isSignedInSelector, onlineStatusChange, serverStatusChange, + executeGA +} from '../../redux/actions'; +import { + isSignedInSelector, + userSelector, isOnlineSelector, isServerOnlineSelector, - userFetchStateSelector, - userSelector, - executeGA -} from '../../redux'; + userFetchStateSelector +} from '../../redux/selectors'; import { UserFetchState, User } from '../../redux/prop-types'; import Flash from '../Flash'; import { flashMessageSelector, removeFlashMessage } from '../Flash/redux'; diff --git a/client/src/components/layouts/learn.tsx b/client/src/components/layouts/learn.tsx index 8f3a01f63cf..46b414c9bba 100644 --- a/client/src/components/layouts/learn.tsx +++ b/client/src/components/layouts/learn.tsx @@ -3,12 +3,12 @@ import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { Loader } from '../../components/helpers'; +import { tryToShowDonationModal } from '../../redux/actions'; import { - userSelector, userFetchStateSelector, isSignedInSelector, - tryToShowDonationModal -} from '../../redux'; + userSelector +} from '../../redux/selectors'; import DonateModal from '../Donation/donation-modal'; import createRedirect from '../create-redirect'; diff --git a/client/src/components/profile/components/certifications.tsx b/client/src/components/profile/components/certifications.tsx index 0350bf390fd..0fe7ddef26c 100644 --- a/client/src/components/profile/components/certifications.tsx +++ b/client/src/components/profile/components/certifications.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { certificatesByNameSelector } from '../../../redux'; +import { certificatesByNameSelector } from '../../../redux/selectors'; import type { CurrentCert } from '../../../redux/prop-types'; import { ButtonSpacer, FullWidthRow, Link, Spacer } from '../../helpers'; import './certifications.css'; diff --git a/client/src/components/profile/components/time-line.tsx b/client/src/components/profile/components/time-line.tsx index f8eb4833576..2a082a3cfa2 100644 --- a/client/src/components/profile/components/time-line.tsx +++ b/client/src/components/profile/components/time-line.tsx @@ -17,7 +17,7 @@ import { regeneratePathAndHistory } from '../../../../../utils/polyvinyl'; import CertificationIcon from '../../../assets/icons/certification-icon'; import { CompletedChallenge } from '../../../redux/prop-types'; import ProjectPreviewModal from '../../../templates/Challenges/components/project-preview-modal'; -import { openModal } from '../../../templates/Challenges/redux'; +import { openModal } from '../../../templates/Challenges/redux/actions'; import { FullWidthRow, Link } from '../../helpers'; import { SolutionDisplayWidget } from '../../solution-display-widget'; import TimelinePagination from './timeline-pagination'; diff --git a/client/src/components/settings/certification.js b/client/src/components/settings/certification.js index 54027261b95..4d970cc195a 100644 --- a/client/src/components/settings/certification.js +++ b/client/src/components/settings/certification.js @@ -10,7 +10,7 @@ import { connect } from 'react-redux'; import { regeneratePathAndHistory } from '../../../../utils/polyvinyl'; import ProjectPreviewModal from '../../templates/Challenges/components/project-preview-modal'; -import { openModal } from '../../templates/Challenges/redux'; +import { openModal } from '../../templates/Challenges/redux/actions'; import { projectMap, legacyProjectMap diff --git a/client/src/components/settings/danger-zone.tsx b/client/src/components/settings/danger-zone.tsx index 4ea1bf48434..3ca233acb02 100644 --- a/client/src/components/settings/danger-zone.tsx +++ b/client/src/components/settings/danger-zone.tsx @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import type { Dispatch } from 'redux'; -import { deleteAccount, resetProgress } from '../../redux/settings'; +import { deleteAccount, resetProgress } from '../../redux/settings/actions'; import { FullWidthRow, ButtonSpacer, Spacer } from '../helpers'; import DeleteModal from './delete-modal'; import ResetModal from './reset-modal'; diff --git a/client/src/components/settings/email.tsx b/client/src/components/settings/email.tsx index 050778f1a7c..15ffd18b81a 100644 --- a/client/src/components/settings/email.tsx +++ b/client/src/components/settings/email.tsx @@ -14,7 +14,7 @@ import { bindActionCreators } from 'redux'; import type { Dispatch } from 'redux'; import isEmail from 'validator/lib/isEmail'; -import { updateMyEmail } from '../../redux/settings'; +import { updateMyEmail } from '../../redux/settings/actions'; import { maybeEmailRE } from '../../utils'; import BlockSaveButton from '../helpers/form/block-save-button'; diff --git a/client/src/components/settings/privacy.tsx b/client/src/components/settings/privacy.tsx index 3c4552d9a9e..2bc13252fda 100644 --- a/client/src/components/settings/privacy.tsx +++ b/client/src/components/settings/privacy.tsx @@ -6,9 +6,9 @@ import { bindActionCreators } from 'redux'; import type { Dispatch } from 'redux'; import { createSelector } from 'reselect'; -import { userSelector } from '../../redux'; +import { userSelector } from '../../redux/selectors'; import type { ProfileUI } from '../../redux/prop-types'; -import { submitProfileUI } from '../../redux/settings'; +import { submitProfileUI } from '../../redux/settings/actions'; import FullWidthRow from '../helpers/full-width-row'; import Spacer from '../helpers/spacer'; diff --git a/client/src/components/settings/user-token.tsx b/client/src/components/settings/user-token.tsx index a356decf882..747f22aa2c2 100644 --- a/client/src/components/settings/user-token.tsx +++ b/client/src/components/settings/user-token.tsx @@ -4,7 +4,7 @@ import React, { Component } from 'react'; import { TFunction, withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; -import { deleteUserToken } from '../../redux'; +import { deleteUserToken } from '../../redux/actions'; import { ButtonSpacer, FullWidthRow, Spacer } from '../helpers'; import './user-token.css'; diff --git a/client/src/components/settings/username.tsx b/client/src/components/settings/username.tsx index c2c0e7daeb7..5c146c20b7d 100644 --- a/client/src/components/settings/username.tsx +++ b/client/src/components/settings/username.tsx @@ -13,11 +13,11 @@ import type { Dispatch } from 'redux'; import { createSelector } from 'reselect'; import { isValidUsername } from '../../../../utils/validate'; +import { usernameValidationSelector } from '../../redux/settings/selectors'; import { validateUsername, - usernameValidationSelector, submitNewUsername -} from '../../redux/settings'; +} from '../../redux/settings/actions'; import BlockSaveButton from '../helpers/form/block-save-button'; import FullWidthRow from '../helpers/full-width-row'; diff --git a/client/src/pages/donate.tsx b/client/src/pages/donate.tsx index 211ebbbf7fb..aaed7962775 100644 --- a/client/src/pages/donate.tsx +++ b/client/src/pages/donate.tsx @@ -17,7 +17,8 @@ import { import { Spacer, Loader } from '../components/helpers'; import CampersImage from '../components/landing/components/campers-image'; -import { signInLoadingSelector, userSelector, executeGA } from '../redux'; +import { executeGA } from '../redux/actions'; +import { signInLoadingSelector, userSelector } from '../redux/selectors'; export interface ExecuteGaArg { type: string; diff --git a/client/src/pages/email-sign-up.tsx b/client/src/pages/email-sign-up.tsx index 259b8affb66..054591daca8 100644 --- a/client/src/pages/email-sign-up.tsx +++ b/client/src/pages/email-sign-up.tsx @@ -11,12 +11,12 @@ import createRedirect from '../components/create-redirect'; import { ButtonSpacer, Spacer, Loader } from '../components/helpers'; import { apiLocation } from '../../../config/env.json'; +import { acceptTerms } from '../redux/actions'; import { - acceptTerms, + signInLoadingSelector, userSelector, - isSignedInSelector, - signInLoadingSelector -} from '../redux'; + isSignedInSelector +} from '../redux/selectors'; import './email-sign-up.css'; interface AcceptPrivacyTermsProps { diff --git a/client/src/pages/learn.tsx b/client/src/pages/learn.tsx index 16e3bea6ff8..96b1faa7fd8 100644 --- a/client/src/pages/learn.tsx +++ b/client/src/pages/learn.tsx @@ -11,10 +11,10 @@ import Map from '../components/Map'; import { Spacer } from '../components/helpers'; import LearnLayout from '../components/layouts/learn'; import { - userFetchStateSelector, isSignedInSelector, - userSelector -} from '../redux'; + userSelector, + userFetchStateSelector +} from '../redux/selectors'; interface FetchState { pending: boolean; diff --git a/client/src/pages/update-email.tsx b/client/src/pages/update-email.tsx index e05ad6ff616..0b9af5fb5ba 100644 --- a/client/src/pages/update-email.tsx +++ b/client/src/pages/update-email.tsx @@ -22,8 +22,8 @@ import isEmail from 'validator/lib/isEmail'; import { Spacer } from '../components/helpers'; import './update-email.css'; -import { userSelector } from '../redux'; -import { updateMyEmail } from '../redux/settings'; +import { userSelector } from '../redux/selectors'; +import { updateMyEmail } from '../redux/settings/actions'; import { maybeEmailRE } from '../utils'; interface UpdateEmailProps { diff --git a/client/src/redux/accept-terms-saga.js b/client/src/redux/accept-terms-saga.js index 0c8092efec0..8397de24416 100644 --- a/client/src/redux/accept-terms-saga.js +++ b/client/src/redux/accept-terms-saga.js @@ -2,9 +2,8 @@ import { navigate } from 'gatsby'; import { call, put, takeEvery } from 'redux-saga/effects'; import { createFlashMessage } from '../components/Flash/redux'; - import { putUserAcceptsTerms } from '../utils/ajax'; -import { acceptTermsComplete, acceptTermsError } from './'; +import { acceptTermsComplete, acceptTermsError } from './actions'; function* acceptTermsSaga({ payload: quincyEmails }) { try { diff --git a/client/src/redux/actions.js b/client/src/redux/actions.js new file mode 100644 index 00000000000..86a4ee014b5 --- /dev/null +++ b/client/src/redux/actions.js @@ -0,0 +1,111 @@ +import { createAction } from 'redux-actions'; + +import { actionTypes } from './action-types'; + +export const appMount = createAction(actionTypes.appMount); + +export const tryToShowDonationModal = createAction( + actionTypes.tryToShowDonationModal +); + +export const executeGA = createAction(actionTypes.executeGA); + +export const allowBlockDonationRequests = createAction( + actionTypes.allowBlockDonationRequests +); +export const closeDonationModal = createAction(actionTypes.closeDonationModal); +export const openDonationModal = createAction(actionTypes.openDonationModal); +export const preventBlockDonationRequests = createAction( + actionTypes.preventBlockDonationRequests +); +export const preventProgressDonationRequests = createAction( + actionTypes.preventProgressDonationRequests +); +export const updateDonationFormState = createAction( + actionTypes.updateDonationFormState +); + +export const onlineStatusChange = createAction(actionTypes.onlineStatusChange); +export const serverStatusChange = createAction(actionTypes.serverStatusChange); + +// TODO: re-evaluate this since /internal is no longer used. +// `hardGoTo` is used to hit the API server directly +// without going through /internal +// used for things like /signin and /signout +export const hardGoTo = createAction(actionTypes.hardGoTo); + +export const submitComplete = createAction(actionTypes.submitComplete); +export const updateComplete = createAction(actionTypes.updateComplete); +export const updateFailed = createAction(actionTypes.updateFailed); + +export const saveChallenge = createAction(actionTypes.saveChallenge); +export const saveChallengeComplete = createAction( + actionTypes.saveChallengeComplete +); + +export const acceptTerms = createAction(actionTypes.acceptTerms); +export const acceptTermsComplete = createAction( + actionTypes.acceptTermsComplete +); +export const acceptTermsError = createAction(actionTypes.acceptTermsError); + +export const fetchUser = createAction(actionTypes.fetchUser); +export const fetchUserComplete = createAction(actionTypes.fetchUserComplete); +export const fetchUserError = createAction(actionTypes.fetchUserError); + +export const addDonation = createAction(actionTypes.addDonation); +export const addDonationComplete = createAction( + actionTypes.addDonationComplete +); +export const addDonationError = createAction(actionTypes.addDonationError); + +export const postChargeStripe = createAction(actionTypes.postChargeStripe); +export const postChargeStripeComplete = createAction( + actionTypes.postChargeStripeComplete +); +export const postChargeStripeError = createAction( + actionTypes.postChargeStripeError +); +export const postChargeStripeCard = createAction( + actionTypes.postChargeStripeCard +); +export const postChargeStripeCardComplete = createAction( + actionTypes.postChargeStripeCardComplete +); +export const postChargeStripeCardError = createAction( + actionTypes.postChargeStripeCardError +); + +export const fetchProfileForUser = createAction( + actionTypes.fetchProfileForUser +); +export const fetchProfileForUserComplete = createAction( + actionTypes.fetchProfileForUserComplete +); +export const fetchProfileForUserError = createAction( + actionTypes.fetchProfileForUserError +); + +export const reportUser = createAction(actionTypes.reportUser); +export const reportUserComplete = createAction(actionTypes.reportUserComplete); +export const reportUserError = createAction(actionTypes.reportUserError); + +export const resetUserData = createAction(actionTypes.resetUserData); + +export const showCert = createAction(actionTypes.showCert); +export const showCertComplete = createAction(actionTypes.showCertComplete); +export const showCertError = createAction(actionTypes.showCertError); + +export const updateUserToken = createAction(actionTypes.updateUserToken); +export const deleteUserToken = createAction(actionTypes.deleteUserToken); +export const deleteUserTokenComplete = createAction( + actionTypes.deleteUserTokenComplete +); + +export const hideCodeAlly = createAction(actionTypes.hideCodeAlly); +export const showCodeAlly = createAction(actionTypes.showCodeAlly); +export const tryToShowCodeAlly = createAction(actionTypes.tryToShowCodeAlly); + +export const updateCurrentChallengeId = createAction( + actionTypes.updateCurrentChallengeId +); diff --git a/client/src/redux/codeally-saga.js b/client/src/redux/codeally-saga.js index 820e6600410..57d1069c52a 100644 --- a/client/src/redux/codeally-saga.js +++ b/client/src/redux/codeally-saga.js @@ -1,13 +1,10 @@ import { call, put, select, takeEvery } from 'redux-saga/effects'; + import { createFlashMessage } from '../components/Flash/redux'; import { FlashMessages } from '../components/Flash/redux/flash-messages'; import { postUserToken } from '../utils/ajax'; -import { - isSignedInSelector, - showCodeAlly, - updateUserToken, - userTokenSelector -} from './'; +import { showCodeAlly, updateUserToken } from './actions'; +import { isSignedInSelector, userTokenSelector } from './selectors'; const startProjectErrMessage = { type: 'danger', diff --git a/client/src/redux/createStore.js b/client/src/redux/createStore.js index edab62b6184..75e19ea5017 100644 --- a/client/src/redux/createStore.js +++ b/client/src/redux/createStore.js @@ -1,4 +1,3 @@ -/* eslint-disable-next-line max-len */ import { createStore as reduxCreateStore, applyMiddleware } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction'; import { createEpicMiddleware } from 'redux-observable'; diff --git a/client/src/redux/donation-saga.js b/client/src/redux/donation-saga.js index 8ec0ca55f95..73f0a9f5822 100644 --- a/client/src/redux/donation-saga.js +++ b/client/src/redux/donation-saga.js @@ -1,13 +1,13 @@ +import i18next from 'i18next'; import { + call, + delay, put, select, + take, takeEvery, - takeLeading, - delay, - call, - take + takeLeading } from 'redux-saga/effects'; -import i18next from 'i18next'; import { addDonation, @@ -15,21 +15,22 @@ import { postChargeStripeCard } from '../utils/ajax'; import { actionTypes as appTypes } from './action-types'; - import { - openDonationModal, - preventBlockDonationRequests, - shouldRequestDonationSelector, - preventProgressDonationRequests, - recentlyClaimedBlockSelector, - isDonatingSelector, addDonationComplete, addDonationError, + openDonationModal, + postChargeStripeCardComplete, + postChargeStripeCardError, postChargeStripeComplete, postChargeStripeError, - postChargeStripeCardComplete, - postChargeStripeCardError -} from './'; + preventBlockDonationRequests, + preventProgressDonationRequests +} from './actions'; +import { + isDonatingSelector, + recentlyClaimedBlockSelector, + shouldRequestDonationSelector +} from './selectors'; const defaultDonationErrorMessage = i18next.t('donate.error-2'); diff --git a/client/src/redux/failed-updates-epic.js b/client/src/redux/failed-updates-epic.js index f8f78feee1a..58b77dbbbdf 100644 --- a/client/src/redux/failed-updates-epic.js +++ b/client/src/redux/failed-updates-epic.js @@ -1,12 +1,12 @@ import { ofType } from 'redux-observable'; -import { merge, empty } from 'rxjs'; +import { empty, merge } from 'rxjs'; import { - tap, + catchError, filter, - map, ignoreElements, + map, switchMap, - catchError + tap } from 'rxjs/operators'; import store from 'store'; import { v4 as uuid } from 'uuid'; @@ -15,11 +15,8 @@ import { backEndProject } from '../../utils/challenge-types'; import { isGoodXHRStatus } from '../templates/Challenges/utils'; import postUpdate$ from '../templates/Challenges/utils/post-update'; import { actionTypes } from './action-types'; -import { - serverStatusChange, - isServerOnlineSelector, - isSignedInSelector -} from './'; +import { serverStatusChange } from './actions'; +import { isServerOnlineSelector, isSignedInSelector } from './selectors'; const key = 'fcc-failed-updates'; diff --git a/client/src/redux/fetch-user-saga.js b/client/src/redux/fetch-user-saga.js index a77a72a74d2..6e9dadac818 100644 --- a/client/src/redux/fetch-user-saga.js +++ b/client/src/redux/fetch-user-saga.js @@ -1,13 +1,13 @@ import { call, put, takeEvery } from 'redux-saga/effects'; import { getSessionUser, getUserProfile } from '../utils/ajax'; -import { jwt } from './cookieValues'; import { - fetchUserComplete, - fetchUserError, + fetchProfileForUserComplete, fetchProfileForUserError, - fetchProfileForUserComplete -} from './'; + fetchUserComplete, + fetchUserError +} from './actions'; +import { jwt } from './cookieValues'; function* fetchSessionUser() { if (!jwt) { diff --git a/client/src/redux/ga-saga.js b/client/src/redux/ga-saga.js index 59505a0e8c2..9b94094a516 100644 --- a/client/src/redux/ga-saga.js +++ b/client/src/redux/ga-saga.js @@ -1,14 +1,15 @@ /* eslint-disable camelcase */ -import { takeEvery, call, all, select } from 'redux-saga/effects'; +import { all, call, select, takeEvery } from 'redux-saga/effects'; + import { aBTestConfig } from '../../../config/donation-settings'; import ga from '../analytics'; -import { - emailSelector, - completionCountSelector, - completedChallengesSelector, - recentlyClaimedBlockSelector -} from '../redux'; import { emailToABVariant } from '../utils/A-B-tester'; +import { + completedChallengesSelector, + completionCountSelector, + emailSelector, + recentlyClaimedBlockSelector +} from './selectors'; const GaTypes = { event: ga.event, page: ga.pageview, modal: ga.modalview }; diff --git a/client/src/redux/index.js b/client/src/redux/index.js index 8fbab2ffec4..a315a2ff93e 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -1,29 +1,27 @@ import { uniqBy } from 'lodash-es'; -import { createAction, handleActions } from 'redux-actions'; +import { handleActions } from 'redux-actions'; import store from 'store'; -import { SuperBlocks } from '../../../config/certification-settings'; -import { actionTypes as challengeTypes } from '../templates/Challenges/redux/action-types'; -import { CURRENT_CHALLENGE_KEY } from '../templates/Challenges/redux/current-challenge-saga'; -import { emailToABVariant } from '../utils/A-B-tester'; +import { + actionTypes as challengeTypes, + CURRENT_CHALLENGE_KEY +} from '../templates/Challenges/redux/action-types'; import { createAcceptTermsSaga } from './accept-terms-saga'; -import { actionTypes } from './action-types'; +import { actionTypes, ns as MainApp } from './action-types'; import { createAppMountSaga } from './app-mount-saga'; import { createCodeAllySaga } from './codeally-saga'; import { createDonationSaga } from './donation-saga'; import failedUpdatesEpic from './failed-updates-epic'; import { createFetchUserSaga } from './fetch-user-saga'; import { createGaSaga } from './ga-saga'; - import hardGoToEpic from './hard-go-to-epic'; import { createReportUserSaga } from './report-user-saga'; +import { createSaveChallengeSaga } from './save-challenge-saga'; +import { savedChallengesSelector } from './selectors'; import { actionTypes as settingsTypes } from './settings/action-types'; import { createShowCertSaga } from './show-cert-saga'; import updateCompleteEpic from './update-complete-epic'; import { createUserTokenSaga } from './user-token-saga'; -import { createSaveChallengeSaga } from './save-challenge-saga'; - -export const MainApp = 'app'; export const defaultFetchState = { pending: true, @@ -43,7 +41,7 @@ export const defaultDonationFormState = { } }; -const initialState = { +export const initialState = { appUsername: '', recentlyClaimedBlock: null, canRequestProgressDonation: true, @@ -85,329 +83,6 @@ export const sagas = [ ...createSaveChallengeSaga(actionTypes) ]; -export const appMount = createAction(actionTypes.appMount); - -export const tryToShowDonationModal = createAction( - actionTypes.tryToShowDonationModal -); - -export const executeGA = createAction(actionTypes.executeGA); - -export const allowBlockDonationRequests = createAction( - actionTypes.allowBlockDonationRequests -); -export const closeDonationModal = createAction(actionTypes.closeDonationModal); -export const openDonationModal = createAction(actionTypes.openDonationModal); -export const preventBlockDonationRequests = createAction( - actionTypes.preventBlockDonationRequests -); -export const preventProgressDonationRequests = createAction( - actionTypes.preventProgressDonationRequests -); -export const updateDonationFormState = createAction( - actionTypes.updateDonationFormState -); - -export const onlineStatusChange = createAction(actionTypes.onlineStatusChange); -export const serverStatusChange = createAction(actionTypes.serverStatusChange); - -// TODO: re-evaluate this since /internal is no longer used. -// `hardGoTo` is used to hit the API server directly -// without going through /internal -// used for things like /signin and /signout -export const hardGoTo = createAction(actionTypes.hardGoTo); - -export const submitComplete = createAction(actionTypes.submitComplete); -export const updateComplete = createAction(actionTypes.updateComplete); -export const updateFailed = createAction(actionTypes.updateFailed); - -export const saveChallenge = createAction(actionTypes.saveChallenge); -export const saveChallengeComplete = createAction( - actionTypes.saveChallengeComplete -); - -export const acceptTerms = createAction(actionTypes.acceptTerms); -export const acceptTermsComplete = createAction( - actionTypes.acceptTermsComplete -); -export const acceptTermsError = createAction(actionTypes.acceptTermsError); - -export const fetchUser = createAction(actionTypes.fetchUser); -export const fetchUserComplete = createAction(actionTypes.fetchUserComplete); -export const fetchUserError = createAction(actionTypes.fetchUserError); - -export const addDonation = createAction(actionTypes.addDonation); -export const addDonationComplete = createAction( - actionTypes.addDonationComplete -); -export const addDonationError = createAction(actionTypes.addDonationError); - -export const postChargeStripe = createAction(actionTypes.postChargeStripe); -export const postChargeStripeComplete = createAction( - actionTypes.postChargeStripeComplete -); -export const postChargeStripeError = createAction( - actionTypes.postChargeStripeError -); -export const postChargeStripeCard = createAction( - actionTypes.postChargeStripeCard -); -export const postChargeStripeCardComplete = createAction( - actionTypes.postChargeStripeCardComplete -); -export const postChargeStripeCardError = createAction( - actionTypes.postChargeStripeCardError -); - -export const fetchProfileForUser = createAction( - actionTypes.fetchProfileForUser -); -export const fetchProfileForUserComplete = createAction( - actionTypes.fetchProfileForUserComplete -); -export const fetchProfileForUserError = createAction( - actionTypes.fetchProfileForUserError -); - -export const reportUser = createAction(actionTypes.reportUser); -export const reportUserComplete = createAction(actionTypes.reportUserComplete); -export const reportUserError = createAction(actionTypes.reportUserError); - -export const resetUserData = createAction(actionTypes.resetUserData); - -export const showCert = createAction(actionTypes.showCert); -export const showCertComplete = createAction(actionTypes.showCertComplete); -export const showCertError = createAction(actionTypes.showCertError); - -export const updateUserToken = createAction(actionTypes.updateUserToken); -export const deleteUserToken = createAction(actionTypes.deleteUserToken); -export const deleteUserTokenComplete = createAction( - actionTypes.deleteUserTokenComplete -); - -export const hideCodeAlly = createAction(actionTypes.hideCodeAlly); -export const showCodeAlly = createAction(actionTypes.showCodeAlly); -export const tryToShowCodeAlly = createAction(actionTypes.tryToShowCodeAlly); - -export const updateCurrentChallengeId = createAction( - actionTypes.updateCurrentChallengeId -); - -export const savedChallengesSelector = state => - userSelector(state).savedChallenges || []; -export const completedChallengesSelector = state => - userSelector(state).completedChallenges || []; -export const partiallyCompletedChallengesSelector = state => - userSelector(state).partiallyCompletedChallenges || []; -export const completionCountSelector = state => state[MainApp].completionCount; -export const currentChallengeIdSelector = state => - state[MainApp].currentChallengeId; - -export const emailSelector = state => userSelector(state).email; -export const isVariantASelector = state => { - const email = emailSelector(state); - // if the user is not signed in and the user info is not available. - // always return A the control variant - if (!email) return true; - return emailToABVariant(email).isVariantA; -}; -export const isDonatingSelector = state => userSelector(state).isDonating; -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[MainApp].recentlyClaimedBlock; -export const donationFormStateSelector = state => - state[MainApp].donationFormState; -export const signInLoadingSelector = state => - userFetchStateSelector(state).pending; -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[MainApp].canRequestProgressDonation; - const isDonating = isDonatingSelector(state); - const recentlyClaimedBlock = recentlyClaimedBlockSelector(state); - - // don't request donation if already donating - if (isDonating) return false; - - // a block has been completed - if (recentlyClaimedBlock) return true; - - // a donation has already been requested - if (!canRequestProgressDonation) return false; - - // donations only appear after the user has completed ten challenges (i.e. - // not before the 11th challenge has mounted) - if (completedChallenges.length < 10) { - return false; - } - - // this will mean we have completed 3 or more challenges this browser session - // and enough challenges overall to not be new - return completionCount >= 3; -}; - -export const userTokenSelector = state => { - return userSelector(state).userToken; -}; - -export const showCodeAllySelector = state => { - return state[MainApp].showCodeAlly; -}; - -export const userByNameSelector = username => state => { - 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; -}; - -export const currentCertsSelector = state => - certificatesByNameSelector(state[MainApp]?.appUsername)(state)?.currentCerts; - -export const certificatesByNameSelector = username => state => { - const { - isRespWebDesignCert, - is2018DataVisCert, - isFrontEndLibsCert, - isJsAlgoDataStructCert, - isApisMicroservicesCert, - isInfosecQaCert, - isQaCertV7, - isInfosecCertV7, - isFrontEndCert, - isBackEndCert, - isDataVisCert, - isFullStackCert, - isSciCompPyCertV7, - isDataAnalysisPyCertV7, - isMachineLearningPyCertV7, - isRelationalDatabaseCertV8 - } = userByNameSelector(username)(state); - return { - hasModernCert: - isRespWebDesignCert || - is2018DataVisCert || - isFrontEndLibsCert || - isJsAlgoDataStructCert || - isApisMicroservicesCert || - isQaCertV7 || - isInfosecCertV7 || - isFullStackCert || - isSciCompPyCertV7 || - isDataAnalysisPyCertV7 || - isMachineLearningPyCertV7 || - isRelationalDatabaseCertV8, - hasLegacyCert: - isFrontEndCert || isBackEndCert || isDataVisCert || isInfosecQaCert, - isFullStackCert, - currentCerts: [ - { - show: isRespWebDesignCert, - title: 'Responsive Web Design Certification', - certSlug: SuperBlocks.RespWebDesign - }, - { - show: isJsAlgoDataStructCert, - title: 'JavaScript Algorithms and Data Structures Certification', - certSlug: SuperBlocks.JsAlgoDataStruct - }, - { - show: isFrontEndLibsCert, - title: 'Front End Development Libraries Certification', - certSlug: SuperBlocks.FrontEndDevLibs - }, - { - show: is2018DataVisCert, - title: 'Data Visualization Certification', - certSlug: SuperBlocks.DataVis - }, - { - show: isApisMicroservicesCert, - title: 'Back End Development and APIs Certification', - certSlug: SuperBlocks.BackEndDevApis - }, - { - show: isQaCertV7, - title: ' Quality Assurance Certification', - certSlug: 'quality-assurance-v7' - }, - { - show: isInfosecCertV7, - title: 'Information Security Certification', - certSlug: 'information-security-v7' - }, - { - show: isSciCompPyCertV7, - title: 'Scientific Computing with Python Certification', - certSlug: 'scientific-computing-with-python-v7' - }, - { - show: isDataAnalysisPyCertV7, - title: 'Data Analysis with Python Certification', - certSlug: 'data-analysis-with-python-v7' - }, - { - show: isMachineLearningPyCertV7, - title: 'Machine Learning with Python Certification', - certSlug: 'machine-learning-with-python-v7' - }, - { - show: isRelationalDatabaseCertV8, - title: 'Relational Database Certification', - certSlug: 'relational-database-v8' - } - ], - legacyCerts: [ - { - show: isFrontEndCert, - title: 'Front End Certification', - certSlug: 'legacy-front-end' - }, - { - show: isBackEndCert, - title: 'Back End Certification', - certSlug: 'legacy-back-end' - }, - { - show: isDataVisCert, - title: 'Data Visualization Certification', - certSlug: 'legacy-data-visualization' - }, - { - show: isInfosecQaCert, - title: 'Information Security and Quality Assurance Certification', - // Keep the current public profile cert slug - certSlug: 'information-security-and-quality-assurance' - }, - { - show: isFullStackCert, - title: 'Full Stack Certification', - // Keep the current public profile cert slug - certSlug: 'full-stack' - } - ] - }; -}; - -export const userFetchStateSelector = state => state[MainApp].userFetchState; -export const userProfileFetchStateSelector = state => - state[MainApp].userProfileFetchState; -export const usernameSelector = state => state[MainApp].appUsername; -export const userSelector = state => { - const username = usernameSelector(state); - - return state[MainApp].user[username] || {}; -}; - -export const sessionMetaSelector = state => state[MainApp].sessionMeta; - function spreadThePayloadOnUser(state, payload) { return { ...state, diff --git a/client/src/redux/report-user-saga.js b/client/src/redux/report-user-saga.js index f3c89c91a02..76a83ca1ae9 100644 --- a/client/src/redux/report-user-saga.js +++ b/client/src/redux/report-user-saga.js @@ -2,9 +2,8 @@ import { navigate } from 'gatsby'; import { call, put, takeEvery } from 'redux-saga/effects'; import { createFlashMessage } from '../components/Flash/redux'; - import { postReportUser } from '../utils/ajax'; -import { reportUserComplete, reportUserError } from './'; +import { reportUserComplete, reportUserError } from './actions'; function* reportUserSaga({ payload }) { try { diff --git a/client/src/redux/rootReducer.js b/client/src/redux/rootReducer.js index c914b2da877..6c20a0672dd 100644 --- a/client/src/redux/rootReducer.js +++ b/client/src/redux/rootReducer.js @@ -1,23 +1,22 @@ import { combineReducers } from 'redux'; +import { reducer as flash } from '../components/Flash/redux'; import { - reducer as flash, - FlashApp as flashNameSpace -} from '../components/Flash/redux'; -import { - reducer as search, - ns as searchNameSpace + ns as searchNameSpace, + reducer as search } from '../components/search/redux'; import { - reducer as challenge, - ns as challengeNameSpace + ns as challengeNameSpace, + reducer as challenge } from '../templates/Challenges/redux'; import { - reducer as curriculumMap, - ns as curriculumMapNameSpace + ns as curriculumMapNameSpace, + reducer as curriculumMap } from '../templates/Introduction/redux'; -import { reducer as settings, ns as settingsNameSpace } from './settings'; -import { reducer as app, MainApp as appNameSpace } from './'; +import { ns as appNameSpace } from './action-types'; +import { ns as settingsNameSpace, reducer as settings } from './settings'; +import { FlashApp as flashNameSpace } from './types'; +import { reducer as app } from './'; export default combineReducers({ [appNameSpace]: app, diff --git a/client/src/redux/save-challenge-saga.js b/client/src/redux/save-challenge-saga.js index 02d938fe2f2..be0ad140992 100644 --- a/client/src/redux/save-challenge-saga.js +++ b/client/src/redux/save-challenge-saga.js @@ -1,19 +1,21 @@ -import { call, takeEvery, put, select } from 'redux-saga/effects'; -import { postSaveChallenge, mapFilesToChallengeFiles } from '../utils/ajax'; +import { call, put, select, takeEvery } from 'redux-saga/effects'; + +import { challengeTypes } from '../../utils/challenge-types'; +import { createFlashMessage } from '../components/Flash/redux'; +import { FlashMessages } from '../components/Flash/redux/flash-messages'; import { challengeDataSelector, challengeMetaSelector -} from '../templates/Challenges/redux'; -import { createFlashMessage } from '../components/Flash/redux'; -import { challengeTypes } from '../../utils/challenge-types'; -import { FlashMessages } from '../components/Flash/redux/flash-messages'; +} from '../templates/Challenges/redux/selectors'; +import { mapFilesToChallengeFiles, postSaveChallenge } from '../utils/ajax'; import { - standardizeRequestBody, - getStringSizeInBytes, bodySizeFits, - MAX_BODY_SIZE + getStringSizeInBytes, + MAX_BODY_SIZE, + standardizeRequestBody } from '../utils/challenge-request-helpers'; -import { saveChallengeComplete, savedChallengesSelector } from './'; +import { saveChallengeComplete } from './actions'; +import { savedChallengesSelector } from './selectors'; export function* saveChallengeSaga() { const { id, challengeType } = yield select(challengeMetaSelector); diff --git a/client/src/redux/selectors.js b/client/src/redux/selectors.js new file mode 100644 index 00000000000..b751b3f0192 --- /dev/null +++ b/client/src/redux/selectors.js @@ -0,0 +1,219 @@ +import { SuperBlocks } from '../../../config/certification-settings'; +import { emailToABVariant } from '../utils/A-B-tester'; +import { ns as MainApp } from './action-types'; + +export const savedChallengesSelector = state => + userSelector(state).savedChallenges || []; +export const completedChallengesSelector = state => + userSelector(state).completedChallenges || []; +export const partiallyCompletedChallengesSelector = state => + userSelector(state).partiallyCompletedChallenges || []; +export const completionCountSelector = state => state[MainApp].completionCount; +export const currentChallengeIdSelector = state => + state[MainApp].currentChallengeId; + +export const emailSelector = state => userSelector(state).email; +export const isVariantASelector = state => { + const email = emailSelector(state); + // if the user is not signed in and the user info is not available. + // always return A the control variant + if (!email) return true; + return emailToABVariant(email).isVariantA; +}; +export const isDonatingSelector = state => userSelector(state).isDonating; +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[MainApp].recentlyClaimedBlock; +export const donationFormStateSelector = state => + state[MainApp].donationFormState; +export const signInLoadingSelector = state => + userFetchStateSelector(state).pending; +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[MainApp].canRequestProgressDonation; + const isDonating = isDonatingSelector(state); + const recentlyClaimedBlock = recentlyClaimedBlockSelector(state); + + // don't request donation if already donating + if (isDonating) return false; + + // a block has been completed + if (recentlyClaimedBlock) return true; + + // a donation has already been requested + if (!canRequestProgressDonation) return false; + + // donations only appear after the user has completed ten challenges (i.e. + // not before the 11th challenge has mounted) + if (completedChallenges.length < 10) { + return false; + } + + // this will mean we have completed 3 or more challenges this browser session + // and enough challenges overall to not be new + return completionCount >= 3; +}; + +export const userTokenSelector = state => { + return userSelector(state).userToken; +}; + +export const showCodeAllySelector = state => { + return state[MainApp].showCodeAlly; +}; + +export const userByNameSelector = username => state => { + const { user } = state[MainApp]; + // return initial state empty user empty object instead of empty + // object litteral to prevent components from re-rendering unnecessarily + // TODO: confirm if "initialState" can be moved here or action-types.js + return user[username] ?? {}; +}; + +export const currentCertsSelector = state => + certificatesByNameSelector(state[MainApp]?.appUsername)(state)?.currentCerts; + +export const certificatesByNameSelector = username => state => { + const { + isRespWebDesignCert, + is2018DataVisCert, + isFrontEndLibsCert, + isJsAlgoDataStructCert, + isApisMicroservicesCert, + isInfosecQaCert, + isQaCertV7, + isInfosecCertV7, + isFrontEndCert, + isBackEndCert, + isDataVisCert, + isFullStackCert, + isSciCompPyCertV7, + isDataAnalysisPyCertV7, + isMachineLearningPyCertV7, + isRelationalDatabaseCertV8 + } = userByNameSelector(username)(state); + return { + hasModernCert: + isRespWebDesignCert || + is2018DataVisCert || + isFrontEndLibsCert || + isJsAlgoDataStructCert || + isApisMicroservicesCert || + isQaCertV7 || + isInfosecCertV7 || + isFullStackCert || + isSciCompPyCertV7 || + isDataAnalysisPyCertV7 || + isMachineLearningPyCertV7 || + isRelationalDatabaseCertV8, + hasLegacyCert: + isFrontEndCert || isBackEndCert || isDataVisCert || isInfosecQaCert, + isFullStackCert, + currentCerts: [ + { + show: isRespWebDesignCert, + title: 'Responsive Web Design Certification', + certSlug: SuperBlocks.RespWebDesign + }, + { + show: isJsAlgoDataStructCert, + title: 'JavaScript Algorithms and Data Structures Certification', + certSlug: SuperBlocks.JsAlgoDataStruct + }, + { + show: isFrontEndLibsCert, + title: 'Front End Development Libraries Certification', + certSlug: SuperBlocks.FrontEndDevLibs + }, + { + show: is2018DataVisCert, + title: 'Data Visualization Certification', + certSlug: SuperBlocks.DataVis + }, + { + show: isApisMicroservicesCert, + title: 'Back End Development and APIs Certification', + certSlug: SuperBlocks.BackEndDevApis + }, + { + show: isQaCertV7, + title: ' Quality Assurance Certification', + certSlug: 'quality-assurance-v7' + }, + { + show: isInfosecCertV7, + title: 'Information Security Certification', + certSlug: 'information-security-v7' + }, + { + show: isSciCompPyCertV7, + title: 'Scientific Computing with Python Certification', + certSlug: 'scientific-computing-with-python-v7' + }, + { + show: isDataAnalysisPyCertV7, + title: 'Data Analysis with Python Certification', + certSlug: 'data-analysis-with-python-v7' + }, + { + show: isMachineLearningPyCertV7, + title: 'Machine Learning with Python Certification', + certSlug: 'machine-learning-with-python-v7' + }, + { + show: isRelationalDatabaseCertV8, + title: 'Relational Database Certification', + certSlug: 'relational-database-v8' + } + ], + legacyCerts: [ + { + show: isFrontEndCert, + title: 'Front End Certification', + certSlug: 'legacy-front-end' + }, + { + show: isBackEndCert, + title: 'Back End Certification', + certSlug: 'legacy-back-end' + }, + { + show: isDataVisCert, + title: 'Data Visualization Certification', + certSlug: 'legacy-data-visualization' + }, + { + show: isInfosecQaCert, + title: 'Information Security and Quality Assurance Certification', + // Keep the current public profile cert slug + certSlug: 'information-security-and-quality-assurance' + }, + { + show: isFullStackCert, + title: 'Full Stack Certification', + // Keep the current public profile cert slug + certSlug: 'full-stack' + } + ] + }; +}; + +export const userFetchStateSelector = state => state[MainApp].userFetchState; +export const userProfileFetchStateSelector = state => + state[MainApp].userProfileFetchState; +export const usernameSelector = state => state[MainApp].appUsername; +export const userSelector = state => { + const username = usernameSelector(state); + + return state[MainApp].user[username] || {}; +}; + +export const sessionMetaSelector = state => state[MainApp].sessionMeta; diff --git a/client/src/redux/settings/actions.js b/client/src/redux/settings/actions.js new file mode 100644 index 00000000000..bb451ca2d96 --- /dev/null +++ b/client/src/redux/settings/actions.js @@ -0,0 +1,109 @@ +import { createAction } from 'redux-actions'; + +import { actionTypes as types } from './action-types'; + +const checkForSuccessPayload = ({ type, payload }) => + type === 'success' ? payload : null; + +export const submitNewAbout = createAction(types.submitNewAbout); +export const submitNewAboutComplete = createAction( + types.submitNewAboutComplete, + checkForSuccessPayload +); +export const submitNewAboutError = createAction(types.submitNewAboutError); + +export const submitNewUsername = createAction(types.submitNewUsername); +export const submitNewUsernameComplete = createAction( + types.submitNewUsernameComplete, + ({ type, username }) => (type === 'success' ? username : null) +); +export const submitNewUsernameError = createAction( + types.submitNewUsernameError +); + +export const submitProfileUI = createAction(types.submitProfileUI); +export const submitProfileUIComplete = createAction( + types.submitProfileUIComplete, + checkForSuccessPayload +); +export const submitProfileUIError = createAction(types.submitProfileUIError); + +export const updateMyEmail = createAction(types.updateMyEmail); +export const updateMyEmailComplete = createAction(types.updateMyEmailComplete); +export const updateMyEmailError = createAction(types.updateMyEmailError); + +export const updateMySocials = createAction(types.updateMySocials); +export const updateMySocialsComplete = createAction( + types.updateMySocialsComplete, + checkForSuccessPayload +); +export const updateMySocialsError = createAction(types.updateMySocialsError); + +export const updateMySound = createAction(types.updateMySound); +export const updateMySoundComplete = createAction( + types.updateMySoundComplete, + checkForSuccessPayload +); +export const updateMySoundError = createAction(types.updateMySoundError); + +export const updateMyTheme = createAction(types.updateMyTheme); +export const updateMyThemeComplete = createAction( + types.updateMyThemeComplete, + checkForSuccessPayload +); +export const updateMyThemeError = createAction(types.updateMyThemeError); + +export const updateMyKeyboardShortcuts = createAction( + types.updateMyKeyboardShortcuts +); +export const updateMyKeyboardShortcutsComplete = createAction( + types.updateMyKeyboardShortcutsComplete, + checkForSuccessPayload +); +export const updateMyKeyboardShortcutsError = createAction( + types.updateMyKeyboardShortcutsError +); + +export const updateMyHonesty = createAction(types.updateMyHonesty); +export const updateMyHonestyComplete = createAction( + types.updateMyHonestyComplete, + checkForSuccessPayload +); +export const updateMyHonestyError = createAction(types.updateMyHonestyError); + +export const updateMyQuincyEmail = createAction(types.updateMyQuincyEmail); +export const updateMyQuincyEmailComplete = createAction( + types.updateMyQuincyEmailComplete, + checkForSuccessPayload +); +export const updateMyQuincyEmailError = createAction( + types.updateMyQuincyEmailError +); + +export const updateMyPortfolio = createAction(types.updateMyPortfolio); +export const updateMyPortfolioComplete = createAction( + types.updateMyPortfolioComplete, + checkForSuccessPayload +); +export const updateMyPortfolioError = createAction( + types.updateMyPortfolioError +); + +export const validateUsername = createAction(types.validateUsername); +export const validateUsernameComplete = createAction( + types.validateUsernameComplete +); +export const validateUsernameError = createAction(types.validateUsernameError); + +export const verifyCert = createAction(types.verifyCert); +export const verifyCertComplete = createAction( + types.verifyCertComplete, + checkForSuccessPayload +); +export const verifyCertError = createAction(types.verifyCertError); + +export const resetProgress = createAction(types.resetProgress); +export const resetProgressError = createAction(types.resetProgressError); + +export const deleteAccount = createAction(types.deleteAccount); +export const deleteAccountError = createAction(types.deleteAccountError); diff --git a/client/src/redux/settings/danger-zone-saga.js b/client/src/redux/settings/danger-zone-saga.js index d9d4e6741ff..40af64d0c9f 100644 --- a/client/src/redux/settings/danger-zone-saga.js +++ b/client/src/redux/settings/danger-zone-saga.js @@ -1,12 +1,12 @@ import { navigate } from 'gatsby'; -import { call, put, takeEvery, take } from 'redux-saga/effects'; +import { call, put, take, takeEvery } from 'redux-saga/effects'; -import { resetUserData, fetchUser } from '../'; import { createFlashMessage } from '../../components/Flash/redux'; import { FlashMessages } from '../../components/Flash/redux/flash-messages'; -import { postResetProgress, postDeleteAccount } from '../../utils/ajax'; +import { postDeleteAccount, postResetProgress } from '../../utils/ajax'; import { actionTypes as appTypes } from '../action-types'; -import { deleteAccountError, resetProgressError } from './'; +import { fetchUser, resetUserData } from '../actions'; +import { deleteAccountError, resetProgressError } from './actions'; function* deleteAccountSaga() { try { diff --git a/client/src/redux/settings/index.js b/client/src/redux/settings/index.js index 43b02258240..b609ef702ed 100644 --- a/client/src/redux/settings/index.js +++ b/client/src/redux/settings/index.js @@ -1,4 +1,4 @@ -import { createAction, handleActions } from 'redux-actions'; +import { handleActions } from 'redux-actions'; import { actionTypes as types, ns } from './action-types'; import { createDangerZoneSaga } from './danger-zone-saga'; import { createSettingsSagas } from './settings-sagas'; @@ -26,114 +26,6 @@ export const sagas = [ ...createDangerZoneSaga(types) ]; -const checkForSuccessPayload = ({ type, payload }) => - type === 'success' ? payload : null; - -export const submitNewAbout = createAction(types.submitNewAbout); -export const submitNewAboutComplete = createAction( - types.submitNewAboutComplete, - checkForSuccessPayload -); -export const submitNewAboutError = createAction(types.submitNewAboutError); - -export const submitNewUsername = createAction(types.submitNewUsername); -export const submitNewUsernameComplete = createAction( - types.submitNewUsernameComplete, - ({ type, username }) => (type === 'success' ? username : null) -); -export const submitNewUsernameError = createAction( - types.submitNewUsernameError -); - -export const submitProfileUI = createAction(types.submitProfileUI); -export const submitProfileUIComplete = createAction( - types.submitProfileUIComplete, - checkForSuccessPayload -); -export const submitProfileUIError = createAction(types.submitProfileUIError); - -export const updateMyEmail = createAction(types.updateMyEmail); -export const updateMyEmailComplete = createAction(types.updateMyEmailComplete); -export const updateMyEmailError = createAction(types.updateMyEmailError); - -export const updateMySocials = createAction(types.updateMySocials); -export const updateMySocialsComplete = createAction( - types.updateMySocialsComplete, - checkForSuccessPayload -); -export const updateMySocialsError = createAction(types.updateMySocialsError); - -export const updateMySound = createAction(types.updateMySound); -export const updateMySoundComplete = createAction( - types.updateMySoundComplete, - checkForSuccessPayload -); -export const updateMySoundError = createAction(types.updateMySoundError); - -export const updateMyTheme = createAction(types.updateMyTheme); -export const updateMyThemeComplete = createAction( - types.updateMyThemeComplete, - checkForSuccessPayload -); -export const updateMyThemeError = createAction(types.updateMyThemeError); - -export const updateMyKeyboardShortcuts = createAction( - types.updateMyKeyboardShortcuts -); -export const updateMyKeyboardShortcutsComplete = createAction( - types.updateMyKeyboardShortcutsComplete, - checkForSuccessPayload -); -export const updateMyKeyboardShortcutsError = createAction( - types.updateMyKeyboardShortcutsError -); - -export const updateMyHonesty = createAction(types.updateMyHonesty); -export const updateMyHonestyComplete = createAction( - types.updateMyHonestyComplete, - checkForSuccessPayload -); -export const updateMyHonestyError = createAction(types.updateMyHonestyError); - -export const updateMyQuincyEmail = createAction(types.updateMyQuincyEmail); -export const updateMyQuincyEmailComplete = createAction( - types.updateMyQuincyEmailComplete, - checkForSuccessPayload -); -export const updateMyQuincyEmailError = createAction( - types.updateMyQuincyEmailError -); - -export const updateMyPortfolio = createAction(types.updateMyPortfolio); -export const updateMyPortfolioComplete = createAction( - types.updateMyPortfolioComplete, - checkForSuccessPayload -); -export const updateMyPortfolioError = createAction( - types.updateMyPortfolioError -); - -export const validateUsername = createAction(types.validateUsername); -export const validateUsernameComplete = createAction( - types.validateUsernameComplete -); -export const validateUsernameError = createAction(types.validateUsernameError); - -export const verifyCert = createAction(types.verifyCert); -export const verifyCertComplete = createAction( - types.verifyCertComplete, - checkForSuccessPayload -); -export const verifyCertError = createAction(types.verifyCertError); - -export const resetProgress = createAction(types.resetProgress); -export const resetProgressError = createAction(types.resetProgressError); - -export const deleteAccount = createAction(types.deleteAccount); -export const deleteAccountError = createAction(types.deleteAccountError); - -export const usernameValidationSelector = state => state[ns].usernameValidation; - export const reducer = handleActions( { [types.submitNewUsernameComplete]: state => ({ diff --git a/client/src/redux/settings/selectors.js b/client/src/redux/settings/selectors.js new file mode 100644 index 00000000000..78fd69e0b5c --- /dev/null +++ b/client/src/redux/settings/selectors.js @@ -0,0 +1,3 @@ +import { ns } from './action-types'; + +export const usernameValidationSelector = state => state[ns].usernameValidation; diff --git a/client/src/redux/settings/settings-sagas.js b/client/src/redux/settings/settings-sagas.js index 1799152cdbd..6b7e27a1715 100644 --- a/client/src/redux/settings/settings-sagas.js +++ b/client/src/redux/settings/settings-sagas.js @@ -1,61 +1,61 @@ import { omit } from 'lodash-es'; import { call, + debounce, put, select, - takeLatest, takeEvery, - debounce + takeLatest } from 'redux-saga/effects'; import store from 'store'; +import { + certTypeIdMap, + certTypes +} from '../../../../config/certification-settings'; import { createFlashMessage } from '../../components/Flash/redux'; +import { certMap } from '../../resources/cert-and-project-map'; import { getUsernameExists, putUpdateMyAbout, - putUpdateMyProfileUI, - putUpdateMyUsername, - putUpdateMySocials, putUpdateMyHonesty, - putUpdateMyQuincyEmail, - putVerifyCert, + putUpdateMyKeyboardShortcuts, putUpdateMyPortfolio, - putUpdateMyTheme, + putUpdateMyProfileUI, + putUpdateMyQuincyEmail, + putUpdateMySocials, putUpdateMySound, - putUpdateMyKeyboardShortcuts + putUpdateMyTheme, + putUpdateMyUsername, + putVerifyCert } from '../../utils/ajax'; -import { certMap } from '../../resources/cert-and-project-map'; -import { completedChallengesSelector } from '..'; +import { completedChallengesSelector } from '../selectors'; import { - certTypes, - certTypeIdMap -} from '../../../../config/certification-settings'; -import { - validateUsernameComplete, - validateUsernameError, submitNewAboutComplete, submitNewAboutError, submitNewUsernameComplete, submitNewUsernameError, submitProfileUIComplete, submitProfileUIError, - verifyCertComplete, - verifyCertError, - updateMySocialsComplete, - updateMySocialsError, - updateMyHonestyError, updateMyHonestyComplete, + updateMyHonestyError, + updateMyKeyboardShortcutsComplete, + updateMyKeyboardShortcutsError, + updateMyPortfolioComplete, + updateMyPortfolioError, updateMyQuincyEmailComplete, updateMyQuincyEmailError, - updateMyPortfolioError, - updateMyPortfolioComplete, - updateMyThemeComplete, - updateMyThemeError, + updateMySocialsComplete, + updateMySocialsError, updateMySoundComplete, updateMySoundError, - updateMyKeyboardShortcutsComplete, - updateMyKeyboardShortcutsError -} from './'; + updateMyThemeComplete, + updateMyThemeError, + validateUsernameComplete, + validateUsernameError, + verifyCertComplete, + verifyCertError +} from './actions'; function* submitNewAboutSaga({ payload }) { try { diff --git a/client/src/redux/settings/update-email-saga.js b/client/src/redux/settings/update-email-saga.js index 9108d8c5f9e..be7b746ea4f 100644 --- a/client/src/redux/settings/update-email-saga.js +++ b/client/src/redux/settings/update-email-saga.js @@ -2,10 +2,9 @@ import { call, put, takeEvery } from 'redux-saga/effects'; import isEmail from 'validator/lib/isEmail'; import { createFlashMessage } from '../../components/Flash/redux'; - import { putUserUpdateEmail } from '../../utils/ajax'; import reallyWeirdErrorMessage from '../../utils/really-weird-error-message'; -import { updateMyEmailComplete, updateMyEmailError } from './'; +import { updateMyEmailComplete, updateMyEmailError } from './actions'; function* updateMyEmailSaga({ payload: email = '' }) { if (!email || !isEmail(email)) { diff --git a/client/src/redux/show-cert-saga.js b/client/src/redux/show-cert-saga.js index 2cb1a4be3c6..fb06f8cafa4 100644 --- a/client/src/redux/show-cert-saga.js +++ b/client/src/redux/show-cert-saga.js @@ -1,9 +1,9 @@ import { navigate } from 'gatsby'; -import { put, takeEvery, call } from 'redux-saga/effects'; +import { call, put, takeEvery } from 'redux-saga/effects'; import { createFlashMessage } from '../components/Flash/redux'; import { getShowCert } from '../utils/ajax'; -import { showCertComplete, showCertError } from '.'; +import { showCertComplete, showCertError } from './actions'; function* getShowCertSaga({ payload: { username, certSlug } }) { try { diff --git a/client/src/redux/types.ts b/client/src/redux/types.ts index 999362df8cc..aa1c4d177f0 100644 --- a/client/src/redux/types.ts +++ b/client/src/redux/types.ts @@ -1,5 +1,13 @@ -import { FlashApp, FlashMessageArg } from '../components/Flash/redux'; -import { MainApp } from '.'; +import { FlashMessages } from '../components/Flash/redux/flash-messages'; +import { ns as MainApp } from './action-types'; + +export const FlashApp = 'flash'; + +export type FlashMessageArg = { + type: string; + message: FlashMessages; + variables?: Record; +}; export interface State { [FlashApp]: FlashState; diff --git a/client/src/redux/update-complete-epic.js b/client/src/redux/update-complete-epic.js index 4953d9f3b71..8ae7fa3d3b5 100644 --- a/client/src/redux/update-complete-epic.js +++ b/client/src/redux/update-complete-epic.js @@ -1,8 +1,9 @@ import { ofType } from 'redux-observable'; -import { mapTo, filter } from 'rxjs/operators'; +import { filter, mapTo } from 'rxjs/operators'; import { actionTypes as types } from './action-types'; -import { serverStatusChange, isServerOnlineSelector } from './'; +import { serverStatusChange } from './actions'; +import { isServerOnlineSelector } from './selectors'; export default function updateCompleteEpic(action$, state$) { return action$.pipe( diff --git a/client/src/redux/user-token-saga.js b/client/src/redux/user-token-saga.js index 0928aff1b24..dff3df3fbd4 100644 --- a/client/src/redux/user-token-saga.js +++ b/client/src/redux/user-token-saga.js @@ -1,8 +1,9 @@ import { call, put, takeEvery } from 'redux-saga/effects'; + import { createFlashMessage } from '../components/Flash/redux'; import { FlashMessages } from '../components/Flash/redux/flash-messages'; import { deleteUserToken } from '../utils/ajax'; -import { deleteUserTokenComplete } from '.'; +import { deleteUserTokenComplete } from './actions'; const message = { deleted: { diff --git a/client/src/templates/Challenges/classic/editor-tabs.tsx b/client/src/templates/Challenges/classic/editor-tabs.tsx index 0c26fd0c589..92bf7d7a05f 100644 --- a/client/src/templates/Challenges/classic/editor-tabs.tsx +++ b/client/src/templates/Challenges/classic/editor-tabs.tsx @@ -4,11 +4,11 @@ import { createSelector } from 'reselect'; import { sortChallengeFiles } from '../../../../../utils/sort-challengefiles'; import { ChallengeFile, ChallengeFiles } from '../../../redux/prop-types'; +import { toggleVisibleEditor } from '../redux/actions'; import { - toggleVisibleEditor, visibleEditorsSelector, challengeFilesSelector -} from '../redux'; +} from '../redux/selectors'; type VisibleEditors = { [key: string]: boolean; diff --git a/client/src/templates/Challenges/classic/editor.tsx b/client/src/templates/Challenges/classic/editor.tsx index 06c243fe1b4..4a42b207082 100644 --- a/client/src/templates/Challenges/classic/editor.tsx +++ b/client/src/templates/Challenges/classic/editor.tsx @@ -24,12 +24,12 @@ import { debounce } from 'lodash-es'; import { useTranslation } from 'react-i18next'; import { Loader } from '../../../components/helpers'; import { Themes } from '../../../components/settings/theme'; +import { saveChallenge } from '../../../redux/actions'; import { - userSelector, - saveChallenge, isDonationModalOpenSelector, - isSignedInSelector -} from '../../../redux'; + isSignedInSelector, + userSelector +} from '../../../redux/selectors'; import { ChallengeFiles, Dimensions, @@ -45,24 +45,26 @@ import { isFinalProject } from '../../../../utils/challenge-types'; import { - canFocusEditorSelector, - challengeMetaSelector, - consoleOutputSelector, executeChallenge, saveEditorContent, setEditorFocusability, updateFile, - challengeTestsSelector, submitChallenge, initTests, - isResettingSelector, stopResetting, - isProjectPreviewModalOpenSelector, openModal, - isChallengeCompletedSelector, - attemptsSelector, resetAttempts -} from '../redux'; +} from '../redux/actions'; +import { + attemptsSelector, + canFocusEditorSelector, + challengeMetaSelector, + consoleOutputSelector, + challengeTestsSelector, + isResettingSelector, + isProjectPreviewModalOpenSelector, + isChallengeCompletedSelector +} from '../redux/selectors'; import GreenPass from '../../../assets/icons/green-pass'; import LowerJaw from './lower-jaw'; diff --git a/client/src/templates/Challenges/classic/multifile-editor.tsx b/client/src/templates/Challenges/classic/multifile-editor.tsx index 5c94a8e6caf..c2d98d6705a 100644 --- a/client/src/templates/Challenges/classic/multifile-editor.tsx +++ b/client/src/templates/Challenges/classic/multifile-editor.tsx @@ -3,12 +3,15 @@ import { connect } from 'react-redux'; import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex'; import { createSelector } from 'reselect'; import { editor } from 'monaco-editor'; -import { isDonationModalOpenSelector, userSelector } from '../../../redux'; +import { + userSelector, + isDonationModalOpenSelector +} from '../../../redux/selectors'; import { canFocusEditorSelector, consoleOutputSelector, visibleEditorsSelector -} from '../redux'; +} from '../redux/selectors'; import { getTargetEditor } from '../utils/get-target-editor'; import './editor.css'; import { diff --git a/client/src/templates/Challenges/classic/show.tsx b/client/src/templates/Challenges/classic/show.tsx index 7ab20924c7a..37d198ad7d1 100644 --- a/client/src/templates/Challenges/classic/show.tsx +++ b/client/src/templates/Challenges/classic/show.tsx @@ -38,21 +38,23 @@ import SidePanel from '../components/side-panel'; import VideoModal from '../components/video-modal'; import { cancelTests, - challengeFilesSelector, challengeMounted, - challengeTestsSelector, - consoleOutputSelector, createFiles, executeChallenge, initConsole, initTests, - isChallengeCompletedSelector, previewMounted, updateChallengeMeta, openModal, setEditorFocusability -} from '../redux'; -import { savedChallengesSelector } from '../../../redux'; +} from '../redux/actions'; +import { + challengeFilesSelector, + challengeTestsSelector, + consoleOutputSelector, + isChallengeCompletedSelector +} from '../redux/selectors'; +import { savedChallengesSelector } from '../../../redux/selectors'; import { getGuideUrl } from '../utils'; import MultifileEditor from './multifile-editor'; import DesktopLayout from './desktop-layout'; diff --git a/client/src/templates/Challenges/codeally/show.tsx b/client/src/templates/Challenges/codeally/show.tsx index e4e0f33ed27..4de918367e9 100644 --- a/client/src/templates/Challenges/codeally/show.tsx +++ b/client/src/templates/Challenges/codeally/show.tsx @@ -19,22 +19,21 @@ import CompletionModal from '../components/completion-modal'; import GreenPass from '../../../assets/icons/green-pass'; import HelpModal from '../components/help-modal'; import Hotkeys from '../components/Hotkeys'; +import { hideCodeAlly, tryToShowCodeAlly } from '../../../redux/actions'; import { completedChallengesSelector, - isSignedInSelector, - hideCodeAlly, partiallyCompletedChallengesSelector, showCodeAllySelector, - tryToShowCodeAlly, + isSignedInSelector, userTokenSelector -} from '../../../redux'; +} from '../../../redux/selectors'; import { challengeMounted, - isChallengeCompletedSelector, updateChallengeMeta, openModal, updateSolutionFormValues -} from '../redux'; +} from '../redux/actions'; +import { isChallengeCompletedSelector } from '../redux/selectors'; import { createFlashMessage } from '../../../components/Flash/redux'; import { ChallengeNode, diff --git a/client/src/templates/Challenges/components/Hotkeys.tsx b/client/src/templates/Challenges/components/Hotkeys.tsx index 3a5b4f7ce1c..a795b09ec30 100644 --- a/client/src/templates/Challenges/components/Hotkeys.tsx +++ b/client/src/templates/Challenges/components/Hotkeys.tsx @@ -5,15 +5,17 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { ChallengeFiles, Test, User } from '../../../redux/prop-types'; -import { userSelector } from '../../../redux'; +import { userSelector } from '../../../redux/selectors'; +import { + setEditorFocusability, + submitChallenge, + openModal +} from '../redux/actions'; import { canFocusEditorSelector, - setEditorFocusability, challengeFilesSelector, - submitChallenge, - challengeTestsSelector, - openModal -} from '../redux'; + challengeTestsSelector +} from '../redux/selectors'; import './hotkeys.css'; import { isFinalProject } from '../../../../utils/challenge-types'; diff --git a/client/src/templates/Challenges/components/ResetModal.tsx b/client/src/templates/Challenges/components/ResetModal.tsx index cef53817d97..80db75daeb7 100644 --- a/client/src/templates/Challenges/components/ResetModal.tsx +++ b/client/src/templates/Challenges/components/ResetModal.tsx @@ -7,8 +7,9 @@ import { bindActionCreators, Dispatch } from 'redux'; import { createSelector } from 'reselect'; // Local Utilities -import { executeGA } from '../../../redux'; -import { isResetModalOpenSelector, closeModal, resetChallenge } from '../redux'; +import { executeGA } from '../../../redux/actions'; +import { closeModal, resetChallenge } from '../redux/actions'; +import { isResetModalOpenSelector } from '../redux/selectors'; // Styles import './reset-modal.css'; diff --git a/client/src/templates/Challenges/components/completion-modal.tsx b/client/src/templates/Challenges/components/completion-modal.tsx index 6ba207e32d6..6437f034e43 100644 --- a/client/src/templates/Challenges/components/completion-modal.tsx +++ b/client/src/templates/Challenges/components/completion-modal.tsx @@ -12,22 +12,18 @@ import { createSelector } from 'reselect'; import { dasherize } from '../../../../../utils/slugs'; import { isFinalProject } from '../../../../utils/challenge-types'; import Login from '../../../components/Header/components/Login'; -import { - isSignedInSelector, - executeGA, - allowBlockDonationRequests -} from '../../../redux'; +import { executeGA, allowBlockDonationRequests } from '../../../redux/actions'; +import { isSignedInSelector } from '../../../redux/selectors'; import { AllChallengeNode, ChallengeFiles } from '../../../redux/prop-types'; +import { closeModal, submitChallenge } from '../redux/actions'; import { - closeModal, - submitChallenge, completedChallengesIds, isCompletionModalOpenSelector, successMessageSelector, challengeFilesSelector, challengeMetaSelector -} from '../redux'; +} from '../redux/selectors'; import CompletionModalBody from './completion-modal-body'; import './completion-modal.css'; diff --git a/client/src/templates/Challenges/components/help-modal.tsx b/client/src/templates/Challenges/components/help-modal.tsx index e8426592c4b..66ed57101aa 100644 --- a/client/src/templates/Challenges/components/help-modal.tsx +++ b/client/src/templates/Challenges/components/help-modal.tsx @@ -7,8 +7,9 @@ import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import envData from '../../../../../config/env.json'; -import { executeGA } from '../../../redux'; -import { createQuestion, closeModal, isHelpModalOpenSelector } from '../redux'; +import { executeGA } from '../../../redux/actions'; +import { createQuestion, closeModal } from '../redux/actions'; +import { isHelpModalOpenSelector } from '../redux/selectors'; import './help-modal.css'; diff --git a/client/src/templates/Challenges/components/preview-portal.tsx b/client/src/templates/Challenges/components/preview-portal.tsx index 11f83a4eafa..4710cf0dc8e 100644 --- a/client/src/templates/Challenges/components/preview-portal.tsx +++ b/client/src/templates/Challenges/components/preview-portal.tsx @@ -2,7 +2,7 @@ import { Component, ReactElement } from 'react'; import ReactDOM from 'react-dom'; import { TFunction, withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; -import { storePortalDocument, removePortalDocument } from '../redux'; +import { storePortalDocument, removePortalDocument } from '../redux/actions'; interface PreviewPortalProps { children: ReactElement | null; diff --git a/client/src/templates/Challenges/components/project-preview-modal.tsx b/client/src/templates/Challenges/components/project-preview-modal.tsx index 7e80db9a6e4..a437b92b120 100644 --- a/client/src/templates/Challenges/components/project-preview-modal.tsx +++ b/client/src/templates/Challenges/components/project-preview-modal.tsx @@ -6,9 +6,9 @@ import type { CompletedChallenge } from '../../../redux/prop-types'; import { closeModal, setEditorFocusability, - isProjectPreviewModalOpenSelector, projectPreviewMounted -} from '../redux'; +} from '../redux/actions'; +import { isProjectPreviewModalOpenSelector } from '../redux/selectors'; import { projectPreviewId } from '../utils/frame'; import Preview from './preview'; diff --git a/client/src/templates/Challenges/components/shortcuts-modal.tsx b/client/src/templates/Challenges/components/shortcuts-modal.tsx index 3c258ee60d7..629a59adff0 100644 --- a/client/src/templates/Challenges/components/shortcuts-modal.tsx +++ b/client/src/templates/Challenges/components/shortcuts-modal.tsx @@ -5,9 +5,10 @@ import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { createSelector } from 'reselect'; -import { closeModal, isShortcutsModalOpenSelector } from '../redux'; -import { updateMyKeyboardShortcuts } from '../../../redux/settings'; -import { userSelector } from '../../../redux'; +import { closeModal } from '../redux/actions'; +import { isShortcutsModalOpenSelector } from '../redux/selectors'; +import { updateMyKeyboardShortcuts } from '../../../redux/settings/actions'; +import { userSelector } from '../../../redux/selectors'; import { User } from '../../../redux/prop-types'; import KeyboardShortcutsSettings from '../../../components/settings/keyboard-shortcuts'; diff --git a/client/src/templates/Challenges/components/side-panel.tsx b/client/src/templates/Challenges/components/side-panel.tsx index 1598f3ce2b2..006b463bc36 100644 --- a/client/src/templates/Challenges/components/side-panel.tsx +++ b/client/src/templates/Challenges/components/side-panel.tsx @@ -4,7 +4,7 @@ import { createSelector } from 'reselect'; import { Test } from '../../../redux/prop-types'; import { mathJaxScriptLoader } from '../../../utils/script-loaders'; -import { challengeTestsSelector } from '../redux'; +import { challengeTestsSelector } from '../redux/selectors'; import TestSuite from './test-suite'; import ToolPanel from './tool-panel'; diff --git a/client/src/templates/Challenges/components/tool-panel.tsx b/client/src/templates/Challenges/components/tool-panel.tsx index ade460b386b..9124ecc504a 100644 --- a/client/src/templates/Challenges/components/tool-panel.tsx +++ b/client/src/templates/Challenges/components/tool-panel.tsx @@ -11,9 +11,11 @@ import { createSelector } from 'reselect'; import { challengeTypes } from '../../../../utils/challenge-types'; import './tool-panel.css'; -import { openModal, executeChallenge, challengeMetaSelector } from '../redux'; +import { openModal, executeChallenge } from '../redux/actions'; +import { challengeMetaSelector } from '../redux/selectors'; -import { saveChallenge, isSignedInSelector } from '../../../redux'; +import { saveChallenge } from '../../../redux/actions'; +import { isSignedInSelector } from '../../../redux/selectors'; const mapStateToProps = createSelector( challengeMetaSelector, diff --git a/client/src/templates/Challenges/components/video-modal.tsx b/client/src/templates/Challenges/components/video-modal.tsx index c0aede8a569..1b42eeff8c8 100644 --- a/client/src/templates/Challenges/components/video-modal.tsx +++ b/client/src/templates/Challenges/components/video-modal.tsx @@ -4,8 +4,9 @@ import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; -import { executeGA } from '../../../redux'; -import { closeModal, isVideoModalOpenSelector } from '../redux'; +import { executeGA } from '../../../redux/actions'; +import { closeModal } from '../redux/actions'; +import { isVideoModalOpenSelector } from '../redux/selectors'; import './video-modal.css'; diff --git a/client/src/templates/Challenges/projects/backend/Show.tsx b/client/src/templates/Challenges/projects/backend/Show.tsx index 3da304fb3b7..d2c05504466 100644 --- a/client/src/templates/Challenges/projects/backend/Show.tsx +++ b/client/src/templates/Challenges/projects/backend/Show.tsx @@ -12,7 +12,7 @@ import { createSelector } from 'reselect'; import Spacer from '../../../../components/helpers/spacer'; import LearnLayout from '../../../../components/layouts/learn'; -import { isSignedInSelector } from '../../../../redux'; +import { isSignedInSelector } from '../../../../redux/selectors'; import { ChallengeMeta, ChallengeNode, @@ -27,15 +27,17 @@ import Output from '../../components/output'; import TestSuite from '../../components/test-suite'; import { challengeMounted, - challengeTestsSelector, - consoleOutputSelector, executeChallenge, initConsole, initTests, - isChallengeCompletedSelector, updateChallengeMeta, updateSolutionFormValues -} from '../../redux'; +} from '../../redux/actions'; +import { + challengeTestsSelector, + consoleOutputSelector, + isChallengeCompletedSelector +} from '../../redux/selectors'; import { getGuideUrl } from '../../utils'; import SolutionForm from '../solution-form'; import ProjectToolPanel from '../tool-panel'; diff --git a/client/src/templates/Challenges/projects/frontend/Show.tsx b/client/src/templates/Challenges/projects/frontend/Show.tsx index f7c85a0802a..a21216b3ded 100644 --- a/client/src/templates/Challenges/projects/frontend/Show.tsx +++ b/client/src/templates/Challenges/projects/frontend/Show.tsx @@ -18,11 +18,11 @@ import CompletionModal from '../../components/completion-modal'; import HelpModal from '../../components/help-modal'; import { challengeMounted, - isChallengeCompletedSelector, updateChallengeMeta, openModal, updateSolutionFormValues -} from '../../redux'; +} from '../../redux/actions'; +import { isChallengeCompletedSelector } from '../../redux/selectors'; import { getGuideUrl } from '../../utils'; import SolutionForm from '../solution-form'; import ProjectToolPanel from '../tool-panel'; diff --git a/client/src/templates/Challenges/projects/tool-panel.tsx b/client/src/templates/Challenges/projects/tool-panel.tsx index 4fd41f9231d..7cb402a04e5 100644 --- a/client/src/templates/Challenges/projects/tool-panel.tsx +++ b/client/src/templates/Challenges/projects/tool-panel.tsx @@ -4,7 +4,7 @@ import { TFunction, withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; -import { openModal } from '../redux'; +import { openModal } from '../redux/actions'; import './tool-panel.css'; diff --git a/client/src/templates/Challenges/redux/action-types.js b/client/src/templates/Challenges/redux/action-types.js index 55607b682dc..caa4b98f5a0 100644 --- a/client/src/templates/Challenges/redux/action-types.js +++ b/client/src/templates/Challenges/redux/action-types.js @@ -1,5 +1,7 @@ import { createTypes } from '../../../utils/create-types'; +export const CURRENT_CHALLENGE_KEY = 'currentChallengeId'; + export const ns = 'challenge'; export const actionTypes = createTypes( diff --git a/client/src/templates/Challenges/redux/actions.js b/client/src/templates/Challenges/redux/actions.js new file mode 100644 index 00000000000..5a488306546 --- /dev/null +++ b/client/src/templates/Challenges/redux/actions.js @@ -0,0 +1,81 @@ +import { createAction } from 'redux-actions'; + +import { getLines } from '../../../../../utils/get-lines'; +import { actionTypes } from './action-types'; + +export const createFiles = createAction( + actionTypes.createFiles, + challengeFiles => + challengeFiles.map(challengeFile => ({ + ...challengeFile, + seed: challengeFile.contents.slice(), + editableContents: getLines( + challengeFile.contents, + challengeFile.editableRegionBoundaries + ), + seedEditableRegionBoundaries: + challengeFile.editableRegionBoundaries?.slice() + })) +); + +export const createQuestion = createAction(actionTypes.createQuestion); +export const initTests = createAction(actionTypes.initTests); +export const updateTests = createAction(actionTypes.updateTests); +export const cancelTests = createAction(actionTypes.cancelTests); + +export const initConsole = createAction(actionTypes.initConsole); +export const initLogs = createAction(actionTypes.initLogs); +export const updateChallengeMeta = createAction( + actionTypes.updateChallengeMeta +); +export const updateFile = createAction(actionTypes.updateFile); +export const updateConsole = createAction(actionTypes.updateConsole); +export const updateLogs = createAction(actionTypes.updateLogs); +export const updateJSEnabled = createAction(actionTypes.updateJSEnabled); +export const updateSolutionFormValues = createAction( + actionTypes.updateSolutionFormValues +); +export const updateSuccessMessage = createAction( + actionTypes.updateSuccessMessage +); + +export const logsToConsole = createAction(actionTypes.logsToConsole); + +export const lockCode = createAction(actionTypes.lockCode); +export const unlockCode = createAction(actionTypes.unlockCode); +export const disableBuildOnError = createAction( + actionTypes.disableBuildOnError +); +export const storedCodeFound = createAction(actionTypes.storedCodeFound); +export const noStoredCodeFound = createAction(actionTypes.noStoredCodeFound); +export const saveEditorContent = createAction(actionTypes.saveEditorContent); + +export const closeModal = createAction(actionTypes.closeModal); +export const openModal = createAction(actionTypes.openModal); + +export const previewMounted = createAction(actionTypes.previewMounted); +export const projectPreviewMounted = createAction( + actionTypes.projectPreviewMounted +); + +export const storePortalDocument = createAction( + actionTypes.storePortalDocument +); +export const removePortalDocument = createAction( + actionTypes.removePortalDocument +); + +export const challengeMounted = createAction(actionTypes.challengeMounted); +export const checkChallenge = createAction(actionTypes.checkChallenge); +export const executeChallenge = createAction(actionTypes.executeChallenge); +export const resetChallenge = createAction(actionTypes.resetChallenge); +export const stopResetting = createAction(actionTypes.stopResetting); +export const submitChallenge = createAction(actionTypes.submitChallenge); +export const resetAttempts = createAction(actionTypes.resetAttempts); + +export const setEditorFocusability = createAction( + actionTypes.setEditorFocusability +); +export const toggleVisibleEditor = createAction( + actionTypes.toggleVisibleEditor +); diff --git a/client/src/templates/Challenges/redux/code-lock-epic.js b/client/src/templates/Challenges/redux/code-lock-epic.js index 5ae23720182..1955e645df1 100644 --- a/client/src/templates/Challenges/redux/code-lock-epic.js +++ b/client/src/templates/Challenges/redux/code-lock-epic.js @@ -1,7 +1,8 @@ import { ofType } from 'redux-observable'; import { map } from 'rxjs/operators'; + import { actionTypes } from './action-types'; -import { unlockCode } from './'; +import { unlockCode } from './actions'; function codeLockEpic(action$) { return action$.pipe(ofType(actionTypes.updateFile), map(unlockCode)); diff --git a/client/src/templates/Challenges/redux/code-storage-epic.js b/client/src/templates/Challenges/redux/code-storage-epic.js index 590eba4f510..dc5e4f27d74 100644 --- a/client/src/templates/Challenges/redux/code-storage-epic.js +++ b/client/src/templates/Challenges/redux/code-storage-epic.js @@ -1,22 +1,20 @@ import { combineEpics, ofType } from 'redux-observable'; import { of } from 'rxjs'; -import { filter, switchMap, map, tap, ignoreElements } from 'rxjs/operators'; +import { filter, ignoreElements, map, switchMap, tap } from 'rxjs/operators'; import store from 'store'; -import { setContent, isPoly } from '../../../../../utils/polyvinyl'; +import { isPoly, setContent } from '../../../../../utils/polyvinyl'; import { createFlashMessage } from '../../../components/Flash/redux'; import { FlashMessages } from '../../../components/Flash/redux/flash-messages'; +import { savedChallengesSelector } from '../../../redux/selectors'; import { actionTypes as appTypes } from '../../../redux/action-types'; -import { savedChallengesSelector } from '../../../redux'; - import { actionTypes } from './action-types'; +import { noStoredCodeFound, storedCodeFound } from './actions'; import { - storedCodeFound, - noStoredCodeFound, - isCodeLockedSelector, challengeFilesSelector, - challengeMetaSelector -} from './'; + challengeMetaSelector, + isCodeLockedSelector +} from './selectors'; const legacyPrefixes = [ 'Bonfire: ', diff --git a/client/src/templates/Challenges/redux/completion-epic.js b/client/src/templates/Challenges/redux/completion-epic.js index 4e2066542a3..337cd9f96e7 100644 --- a/client/src/templates/Challenges/redux/completion-epic.js +++ b/client/src/templates/Challenges/redux/completion-epic.js @@ -1,31 +1,28 @@ import { navigate } from 'gatsby'; import { omit } from 'lodash-es'; import { ofType } from 'redux-observable'; -import { of, empty } from 'rxjs'; -import { switchMap, retry, catchError, concat, tap } from 'rxjs/operators'; +import { empty, of } from 'rxjs'; +import { catchError, concat, retry, switchMap, tap } from 'rxjs/operators'; import { challengeTypes, submitTypes } from '../../../../utils/challenge-types'; +import { actionTypes as submitActionTypes } from '../../../redux/action-types'; import { - userSelector, - isSignedInSelector, submitComplete, updateComplete, updateFailed -} from '../../../redux'; - -import postUpdate$ from '../utils/post-update'; +} from '../../../redux/actions'; +import { isSignedInSelector, userSelector } from '../../../redux/selectors'; import { mapFilesToChallengeFiles } from '../../../utils/ajax'; import { standardizeRequestBody } from '../../../utils/challenge-request-helpers'; -import { actionTypes as submitActionTypes } from '../../../redux/action-types'; +import postUpdate$ from '../utils/post-update'; import { actionTypes } from './action-types'; +import { closeModal, updateSolutionFormValues } from './actions'; import { - projectFormValuesSelector, + challengeFilesSelector, challengeMetaSelector, challengeTestsSelector, - closeModal, - challengeFilesSelector, - updateSolutionFormValues -} from './'; + projectFormValuesSelector +} from './selectors'; function postChallenge(update, username) { const saveChallenge = postUpdate$(update).pipe( diff --git a/client/src/templates/Challenges/redux/create-question-epic.js b/client/src/templates/Challenges/redux/create-question-epic.js index e6111099cdc..e01f001147a 100644 --- a/client/src/templates/Challenges/redux/create-question-epic.js +++ b/client/src/templates/Challenges/redux/create-question-epic.js @@ -1,15 +1,16 @@ import i18next from 'i18next'; import { ofType } from 'redux-observable'; -import { tap, mapTo } from 'rxjs/operators'; +import { mapTo, tap } from 'rxjs/operators'; + import envData from '../../../../../config/env.json'; +import { transformEditorLink } from '../utils'; +import { actionTypes } from './action-types'; +import { closeModal } from './actions'; import { - closeModal, challengeFilesSelector, challengeMetaSelector, projectFormValuesSelector -} from '../redux'; -import { transformEditorLink } from '../utils'; -import { actionTypes } from './action-types'; +} from './selectors'; const { forumLocation } = envData; diff --git a/client/src/templates/Challenges/redux/current-challenge-saga.js b/client/src/templates/Challenges/redux/current-challenge-saga.js index cf1e61328e9..eb78a2f09f4 100644 --- a/client/src/templates/Challenges/redux/current-challenge-saga.js +++ b/client/src/templates/Challenges/redux/current-challenge-saga.js @@ -2,9 +2,8 @@ import { put, takeEvery } from 'redux-saga/effects'; import store from 'store'; import { randomCompliment } from '../../../utils/get-words'; -import { updateSuccessMessage } from './'; - -export const CURRENT_CHALLENGE_KEY = 'currentChallengeId'; +import { CURRENT_CHALLENGE_KEY } from './action-types'; +import { updateSuccessMessage } from './actions'; export function* currentChallengeSaga({ payload: id }) { yield store.set(CURRENT_CHALLENGE_KEY, id); diff --git a/client/src/templates/Challenges/redux/execute-challenge-saga.js b/client/src/templates/Challenges/redux/execute-challenge-saga.js index fd171e041c1..ebf84abbc30 100644 --- a/client/src/templates/Challenges/redux/execute-challenge-saga.js +++ b/client/src/templates/Challenges/redux/execute-challenge-saga.js @@ -2,54 +2,56 @@ import i18next from 'i18next'; import { escape } from 'lodash-es'; import { channel } from 'redux-saga'; import { - delay, - put, - select, call, - takeLatest, - takeEvery, + cancel, + delay, fork, getContext, + put, + select, take, - cancel + takeEvery, + takeLatest } from 'redux-saga/effects'; -import { playTone } from '../../../utils/tone'; -import { - buildChallenge, - canBuildChallenge, - getTestRunner, - challengeHasPreview, - updatePreview, - updateProjectPreview, - isJavaScriptChallenge, - isLoopProtected -} from '../utils/build'; import { challengeTypes } from '../../../../utils/challenge-types'; import { createFlashMessage } from '../../../components/Flash/redux'; import { FlashMessages } from '../../../components/Flash/redux/flash-messages'; import { - standardizeRequestBody, - getStringSizeInBytes, bodySizeFits, - MAX_BODY_SIZE + getStringSizeInBytes, + MAX_BODY_SIZE, + standardizeRequestBody } from '../../../utils/challenge-request-helpers'; +import { playTone } from '../../../utils/tone'; +import { + buildChallenge, + canBuildChallenge, + challengeHasPreview, + getTestRunner, + isJavaScriptChallenge, + isLoopProtected, + updatePreview, + updateProjectPreview +} from '../utils/build'; import { actionTypes } from './action-types'; import { - portalDocumentSelector, + disableBuildOnError, + initConsole, + initLogs, + logsToConsole, + openModal, + updateConsole, + updateLogs, + updateTests +} from './actions'; +import { challengeDataSelector, challengeMetaSelector, challengeTestsSelector, - initConsole, - updateConsole, - initLogs, - updateLogs, - logsToConsole, - updateTests, - openModal, isBuildEnabledSelector, - disableBuildOnError -} from './'; + portalDocumentSelector +} from './selectors'; // How long before bailing out of a preview. const previewTimeout = 2500; diff --git a/client/src/templates/Challenges/redux/index.js b/client/src/templates/Challenges/redux/index.js index 0ffa0a53ac9..8205211c393 100644 --- a/client/src/templates/Challenges/redux/index.js +++ b/client/src/templates/Challenges/redux/index.js @@ -1,9 +1,7 @@ import { isEmpty } from 'lodash-es'; -import { createAction, handleActions } from 'redux-actions'; +import { handleActions } from 'redux-actions'; import { getLines } from '../../../../../utils/get-lines'; -import { challengeTypes } from '../../../../utils/challenge-types'; -import { completedChallengesSelector } from '../../../redux'; import { getTargetEditor } from '../utils/get-target-editor'; import { actionTypes, ns } from './action-types'; import codeLockEpic from './code-lock-epic'; @@ -60,167 +58,6 @@ export const sagas = [ ...createCurrentChallengeSaga(actionTypes) ]; -export const createFiles = createAction( - actionTypes.createFiles, - challengeFiles => - challengeFiles.map(challengeFile => ({ - ...challengeFile, - seed: challengeFile.contents.slice(), - editableContents: getLines( - challengeFile.contents, - challengeFile.editableRegionBoundaries - ), - seedEditableRegionBoundaries: - challengeFile.editableRegionBoundaries?.slice() - })) -); - -export const createQuestion = createAction(actionTypes.createQuestion); -export const initTests = createAction(actionTypes.initTests); -export const updateTests = createAction(actionTypes.updateTests); -export const cancelTests = createAction(actionTypes.cancelTests); - -export const initConsole = createAction(actionTypes.initConsole); -export const initLogs = createAction(actionTypes.initLogs); -export const updateChallengeMeta = createAction( - actionTypes.updateChallengeMeta -); -export const updateFile = createAction(actionTypes.updateFile); -export const updateConsole = createAction(actionTypes.updateConsole); -export const updateLogs = createAction(actionTypes.updateLogs); -export const updateJSEnabled = createAction(actionTypes.updateJSEnabled); -export const updateSolutionFormValues = createAction( - actionTypes.updateSolutionFormValues -); -export const updateSuccessMessage = createAction( - actionTypes.updateSuccessMessage -); - -export const logsToConsole = createAction(actionTypes.logsToConsole); - -export const lockCode = createAction(actionTypes.lockCode); -export const unlockCode = createAction(actionTypes.unlockCode); -export const disableBuildOnError = createAction( - actionTypes.disableBuildOnError -); -export const storedCodeFound = createAction(actionTypes.storedCodeFound); -export const noStoredCodeFound = createAction(actionTypes.noStoredCodeFound); -export const saveEditorContent = createAction(actionTypes.saveEditorContent); - -export const closeModal = createAction(actionTypes.closeModal); -export const openModal = createAction(actionTypes.openModal); - -export const previewMounted = createAction(actionTypes.previewMounted); -export const projectPreviewMounted = createAction( - actionTypes.projectPreviewMounted -); - -export const storePortalDocument = createAction( - actionTypes.storePortalDocument -); -export const removePortalDocument = createAction( - actionTypes.removePortalDocument -); - -export const challengeMounted = createAction(actionTypes.challengeMounted); -export const checkChallenge = createAction(actionTypes.checkChallenge); -export const executeChallenge = createAction(actionTypes.executeChallenge); -export const resetChallenge = createAction(actionTypes.resetChallenge); -export const stopResetting = createAction(actionTypes.stopResetting); -export const submitChallenge = createAction(actionTypes.submitChallenge); -export const resetAttempts = createAction(actionTypes.resetAttempts); - -export const setEditorFocusability = createAction( - actionTypes.setEditorFocusability -); -export const toggleVisibleEditor = createAction( - actionTypes.toggleVisibleEditor -); - -export const currentTabSelector = state => state[ns].currentTab; -export const challengeFilesSelector = state => state[ns].challengeFiles; -export const challengeMetaSelector = state => state[ns].challengeMeta; -export const challengeTestsSelector = state => state[ns].challengeTests; -export const consoleOutputSelector = state => state[ns].consoleOut; -export const completedChallengesIds = state => - completedChallengesSelector(state).map(node => node.id); -export const isChallengeCompletedSelector = state => { - const completedChallenges = completedChallengesSelector(state); - const { id: currentChallengeId } = challengeMetaSelector(state); - return completedChallenges.some(({ id }) => id === currentChallengeId); -}; -export const isCodeLockedSelector = state => state[ns].isCodeLocked; -export const isCompletionModalOpenSelector = state => - state[ns].modal.completion; -export const isHelpModalOpenSelector = state => state[ns].modal.help; -export const isVideoModalOpenSelector = state => state[ns].modal.video; -export const isResetModalOpenSelector = state => state[ns].modal.reset; -export const isProjectPreviewModalOpenSelector = state => - state[ns].modal.projectPreview; -export const isShortcutsModalOpenSelector = state => state[ns].modal.shortcuts; -export const isResettingSelector = state => state[ns].isResetting; - -export const isBuildEnabledSelector = state => state[ns].isBuildEnabled; -export const successMessageSelector = state => state[ns].successMessage; - -export const projectFormValuesSelector = state => - state[ns].projectFormValues || {}; - -export const portalDocumentSelector = state => state[ns].portalDocument; - -export const challengeDataSelector = state => { - const { challengeType } = challengeMetaSelector(state); - let challengeData = { challengeType }; - if ( - challengeType === challengeTypes.js || - challengeType === challengeTypes.jsProject - ) { - challengeData = { - ...challengeData, - challengeFiles: challengeFilesSelector(state) - }; - } else if (challengeType === challengeTypes.backend) { - const { solution: url = {} } = projectFormValuesSelector(state); - challengeData = { - ...challengeData, - url - }; - } else if ( - challengeType === challengeTypes.backEndProject || - challengeType === challengeTypes.pythonProject - ) { - const values = projectFormValuesSelector(state); - const { solution: url } = values; - challengeData = { - ...challengeData, - ...values, - url - }; - } else if (challengeType === challengeTypes.frontEndProject) { - challengeData = { - ...challengeData, - ...projectFormValuesSelector(state) - }; - } else if ( - challengeType === challengeTypes.html || - challengeType === challengeTypes.modern || - challengeType === challengeTypes.multifileCertProject - ) { - const { required = [], template = '' } = challengeMetaSelector(state); - challengeData = { - ...challengeData, - challengeFiles: challengeFilesSelector(state), - required, - template - }; - } - return challengeData; -}; - -export const attemptsSelector = state => state[ns].attempts; -export const canFocusEditorSelector = state => state[ns].canFocusEditor; -export const visibleEditorsSelector = state => state[ns].visibleEditors; - export const reducer = handleActions( { [actionTypes.createFiles]: (state, { payload }) => ({ diff --git a/client/src/templates/Challenges/redux/selectors.js b/client/src/templates/Challenges/redux/selectors.js new file mode 100644 index 00000000000..a5f9296223b --- /dev/null +++ b/client/src/templates/Challenges/redux/selectors.js @@ -0,0 +1,87 @@ +import { challengeTypes } from '../../../../utils/challenge-types'; +import { completedChallengesSelector } from '../../../redux/selectors'; +import { ns } from './action-types'; + +export const currentTabSelector = state => state[ns].currentTab; +export const challengeFilesSelector = state => state[ns].challengeFiles; +export const challengeMetaSelector = state => state[ns].challengeMeta; +export const challengeTestsSelector = state => state[ns].challengeTests; +export const consoleOutputSelector = state => state[ns].consoleOut; +export const completedChallengesIds = state => + completedChallengesSelector(state).map(node => node.id); +export const isChallengeCompletedSelector = state => { + const completedChallenges = completedChallengesSelector(state); + const { id: currentChallengeId } = challengeMetaSelector(state); + return completedChallenges.some(({ id }) => id === currentChallengeId); +}; +export const isCodeLockedSelector = state => state[ns].isCodeLocked; +export const isCompletionModalOpenSelector = state => + state[ns].modal.completion; +export const isHelpModalOpenSelector = state => state[ns].modal.help; +export const isVideoModalOpenSelector = state => state[ns].modal.video; +export const isResetModalOpenSelector = state => state[ns].modal.reset; +export const isProjectPreviewModalOpenSelector = state => + state[ns].modal.projectPreview; +export const isShortcutsModalOpenSelector = state => state[ns].modal.shortcuts; +export const isResettingSelector = state => state[ns].isResetting; + +export const isBuildEnabledSelector = state => state[ns].isBuildEnabled; +export const successMessageSelector = state => state[ns].successMessage; + +export const projectFormValuesSelector = state => + state[ns].projectFormValues || {}; + +export const portalDocumentSelector = state => state[ns].portalDocument; + +export const challengeDataSelector = state => { + const { challengeType } = challengeMetaSelector(state); + let challengeData = { challengeType }; + if ( + challengeType === challengeTypes.js || + challengeType === challengeTypes.jsProject + ) { + challengeData = { + ...challengeData, + challengeFiles: challengeFilesSelector(state) + }; + } else if (challengeType === challengeTypes.backend) { + const { solution: url = {} } = projectFormValuesSelector(state); + challengeData = { + ...challengeData, + url + }; + } else if ( + challengeType === challengeTypes.backEndProject || + challengeType === challengeTypes.pythonProject + ) { + const values = projectFormValuesSelector(state); + const { solution: url } = values; + challengeData = { + ...challengeData, + ...values, + url + }; + } else if (challengeType === challengeTypes.frontEndProject) { + challengeData = { + ...challengeData, + ...projectFormValuesSelector(state) + }; + } else if ( + challengeType === challengeTypes.html || + challengeType === challengeTypes.modern || + challengeType === challengeTypes.multifileCertProject + ) { + const { required = [], template = '' } = challengeMetaSelector(state); + challengeData = { + ...challengeData, + challengeFiles: challengeFilesSelector(state), + required, + template + }; + } + return challengeData; +}; + +export const attemptsSelector = state => state[ns].attempts; +export const canFocusEditorSelector = state => state[ns].canFocusEditor; +export const visibleEditorsSelector = state => state[ns].visibleEditors; diff --git a/client/src/templates/Challenges/video/Show.tsx b/client/src/templates/Challenges/video/Show.tsx index ecf7bda07b9..eefd2b3111e 100644 --- a/client/src/templates/Challenges/video/Show.tsx +++ b/client/src/templates/Challenges/video/Show.tsx @@ -22,12 +22,12 @@ import ChallengeTitle from '../components/challenge-title'; import CompletionModal from '../components/completion-modal'; import PrismFormatted from '../components/prism-formatted'; import { - isChallengeCompletedSelector, challengeMounted, updateChallengeMeta, openModal, updateSolutionFormValues -} from '../redux'; +} from '../redux/actions'; +import { isChallengeCompletedSelector } from '../redux/selectors'; // Styles import './show.css'; diff --git a/client/src/templates/Introduction/components/block.tsx b/client/src/templates/Introduction/components/block.tsx index 3f50672b7fa..3926bde7406 100644 --- a/client/src/templates/Introduction/components/block.tsx +++ b/client/src/templates/Introduction/components/block.tsx @@ -13,7 +13,8 @@ import DropDown from '../../../assets/icons/dropdown'; import GreenNotCompleted from '../../../assets/icons/green-not-completed'; import GreenPass from '../../../assets/icons/green-pass'; import { Link, Spacer } from '../../../components/helpers'; -import { completedChallengesSelector, executeGA } from '../../../redux'; +import { executeGA } from '../../../redux/actions'; +import { completedChallengesSelector } from '../../../redux/selectors'; import { ChallengeNode, CompletedChallenge } from '../../../redux/prop-types'; import { playTone } from '../../../utils/tone'; import { makeExpandedBlockSelector, toggleBlock } from '../redux'; diff --git a/client/src/templates/Introduction/components/cert-challenge.tsx b/client/src/templates/Introduction/components/cert-challenge.tsx index 7a8d1f2f731..58f57e949ef 100644 --- a/client/src/templates/Introduction/components/cert-challenge.tsx +++ b/client/src/templates/Introduction/components/cert-challenge.tsx @@ -12,12 +12,12 @@ import { import { createFlashMessage } from '../../../components/Flash/redux'; import { FlashMessages } from '../../../components/Flash/redux/flash-messages'; import { - userFetchStateSelector, isSignedInSelector, + userFetchStateSelector, currentCertsSelector -} from '../../../redux'; +} from '../../../redux/selectors'; import { User, Steps } from '../../../redux/prop-types'; -import { verifyCert } from '../../../redux/settings'; +import { verifyCert } from '../../../redux/settings/actions'; import { certMap } from '../../../resources/cert-and-project-map'; interface CertChallengeProps { diff --git a/client/src/templates/Introduction/components/challenges.tsx b/client/src/templates/Introduction/components/challenges.tsx index f34d97b4c3f..3aae6aa3da2 100644 --- a/client/src/templates/Introduction/components/challenges.tsx +++ b/client/src/templates/Introduction/components/challenges.tsx @@ -7,7 +7,7 @@ import type { Dispatch } from 'redux'; import GreenNotCompleted from '../../../assets/icons/green-not-completed'; import GreenPass from '../../../assets/icons/green-pass'; -import { executeGA } from '../../../redux'; +import { executeGA } from '../../../redux/actions'; import { SuperBlocks } from '../../../../../config/certification-settings'; import { ExecuteGaArg } from '../../../pages/donate'; import { ChallengeWithCompletedNode } from '../../../redux/prop-types'; diff --git a/client/src/templates/Introduction/super-block-intro.tsx b/client/src/templates/Introduction/super-block-intro.tsx index c4fc0f6ef49..493e6a1bdc9 100644 --- a/client/src/templates/Introduction/super-block-intro.tsx +++ b/client/src/templates/Introduction/super-block-intro.tsx @@ -15,14 +15,14 @@ import DonateModal from '../../components/Donation/donation-modal'; import Login from '../../components/Header/components/Login'; import Map from '../../components/Map'; import { Spacer } from '../../components/helpers'; +import { tryToShowDonationModal } from '../../redux/actions'; import { + isSignedInSelector, + userSelector, currentChallengeIdSelector, userFetchStateSelector, - signInLoadingSelector, - isSignedInSelector, - tryToShowDonationModal, - userSelector -} from '../../redux'; + signInLoadingSelector +} from '../../redux/selectors'; import { MarkdownRemark, AllChallengeNode, User } from '../../redux/prop-types'; import Block from './components/block'; import CertChallenge from './components/cert-challenge'; diff --git a/tools/challenge-helper-scripts/create-project.ts b/tools/challenge-helper-scripts/create-project.ts index 38ac73048e2..1f94d5ac15c 100644 --- a/tools/challenge-helper-scripts/create-project.ts +++ b/tools/challenge-helper-scripts/create-project.ts @@ -9,6 +9,7 @@ import { SuperBlocks } from '../../config/certification-settings'; import { blockNameify } from '../../utils/block-nameify'; import { createStepFile } from './utils'; import { getSuperBlockSubPath } from './fs-utils'; +import { Meta } from './helpers/project-metadata'; const helpCategories = ['HTML-CSS', 'JavaScript', 'Python'] as const; @@ -23,20 +24,6 @@ type SuperBlockInfo = { type IntroJson = Record; -export type Meta = { - name: string; - isUpcomingChange: boolean; - dashedName: string; - order: number; - time: string; - template: string; - required: string[]; - superBlock: string; - superOrder: number; - isBeta: boolean; - challengeOrder: string[][]; -}; - interface CreateProjectArgs { superBlock: SuperBlocks; block: string; diff --git a/tools/challenge-helper-scripts/helpers/project-metadata.ts b/tools/challenge-helper-scripts/helpers/project-metadata.ts index 552c00572c0..2640ed54c2c 100644 --- a/tools/challenge-helper-scripts/helpers/project-metadata.ts +++ b/tools/challenge-helper-scripts/helpers/project-metadata.ts @@ -1,9 +1,22 @@ import fs from 'fs'; import path from 'path'; import glob from 'glob'; -import { Meta } from '../create-project'; import { getProjectName, getProjectPath } from './get-project-info'; +export type Meta = { + name: string; + isUpcomingChange: boolean; + dashedName: string; + order: number; + time: string; + template: string; + required: string[]; + superBlock: string; + superOrder: number; + isBeta: boolean; + challengeOrder: string[][]; +}; + function getMetaData(): Meta { const metaData = fs.readFileSync(getProjectMetaPath(), 'utf8'); return JSON.parse(metaData) as Meta;