fix: circular dependencies (#46638)

* fix: circular dependency in formHelpers

* fix: circular dependency in Flash

* fix: circular dependency in helpers

* fix: circular dependency in current-challenge-saga (1/2)

* fix: circular dependency in code-lock-epic

* feat: eslint rule for circular dependencies

* fix: circular dependency in tools

* fix: import error and more circular dependencies in formHelpers

* fix prettier error

* fix: circular dependency in danger-zone-saga

* fix: circular dependency in update-email-saga

* fix: circular dependency in settings-saga

* fix: circular dependency in accept-terms-saga

* fix: circular dependency in codeally-saga

TODO: make a separate file for selectors

* fix: ci lint error

* fix: ci lint:prettier error

* fix: circular dependency in current-challenge-saga

* fix: cricular dependency in Challenges/redux

* fix: circular dependency in redux/settings

* fix: circular dependency in root redux

* fix: lint errors

* chore: remove unecessary comment

* chore: remove unused export

* fix: clear out actions and selectors

* revert changes to package.json

* test ci with maxDepth of 1

* fix: fallback to empty object

* test action if it will run for maxDepth of 2
This commit is contained in:
Niraj Nandish
2022-10-05 18:08:40 +01:00
committed by GitHub
parent 1f956781e9
commit 53164e71f1
92 changed files with 1055 additions and 1028 deletions
+1
View File
@@ -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"
},
@@ -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';
@@ -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 {
@@ -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';
@@ -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;
+4 -4
View File
@@ -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 {
@@ -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';
@@ -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';
@@ -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';
+6 -9
View File
@@ -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<string, unknown>;
};
const initialState = {
message: {
id: '',
@@ -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;
@@ -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';
@@ -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(
+1 -1
View File
@@ -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;
@@ -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;
+65 -14
View File
@@ -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;
+2 -67
View File
@@ -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 };
@@ -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;
-1
View File
@@ -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';
@@ -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;
+7 -5
View File
@@ -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';
+3 -3
View File
@@ -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';
@@ -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';
@@ -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';
@@ -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
@@ -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';
+1 -1
View File
@@ -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';
+2 -2
View File
@@ -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';
@@ -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';
+2 -2
View File
@@ -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';
+2 -1
View File
@@ -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;
+4 -4
View File
@@ -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 {
+3 -3
View File
@@ -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;
+2 -2
View File
@@ -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 {
+1 -2
View File
@@ -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 {
+111
View File
@@ -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
);
+3 -6
View File
@@ -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',
-1
View File
@@ -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';
+16 -15
View File
@@ -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');
+6 -9
View File
@@ -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';
+5 -5
View File
@@ -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) {
+8 -7
View File
@@ -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 };
+9 -334
View File
@@ -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,
+1 -2
View File
@@ -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 {
+11 -12
View File
@@ -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,
+12 -10
View File
@@ -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);
+219
View File
@@ -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;
+109
View File
@@ -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);
@@ -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 {
+1 -109
View File
@@ -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 => ({
+3
View File
@@ -0,0 +1,3 @@
import { ns } from './action-types';
export const usernameValidationSelector = state => state[ns].usernameValidation;
+29 -29
View File
@@ -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 {
@@ -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)) {
+2 -2
View File
@@ -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 {
+10 -2
View File
@@ -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<string, unknown>;
};
export interface State {
[FlashApp]: FlashState;
+3 -2
View File
@@ -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(
+2 -1
View File
@@ -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: {
@@ -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;
@@ -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';
@@ -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 {
@@ -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';
@@ -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,
@@ -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';
@@ -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';
@@ -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';
@@ -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';
@@ -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;
@@ -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';
@@ -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';
@@ -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';
@@ -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,
@@ -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';
@@ -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';
@@ -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';
@@ -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';
@@ -1,5 +1,7 @@
import { createTypes } from '../../../utils/create-types';
export const CURRENT_CHALLENGE_KEY = 'currentChallengeId';
export const ns = 'challenge';
export const actionTypes = createTypes(
@@ -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
);
@@ -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));
@@ -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: ',
@@ -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(
@@ -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;
@@ -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);
@@ -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;
+1 -164
View File
@@ -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 }) => ({
@@ -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;
@@ -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';
@@ -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';
@@ -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 {
@@ -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';
@@ -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';
@@ -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<SuperBlocks, SuperBlockInfo>;
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;
@@ -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;