mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-23 21:04:36 -05:00
refactor(client): store session user in dedicated key (#59954)
This commit is contained in:
committed by
GitHub
parent
df32414406
commit
3e1da8f3fb
@@ -21,10 +21,11 @@ import {
|
|||||||
showCertFetchStateSelector,
|
showCertFetchStateSelector,
|
||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
isDonatingSelector,
|
isDonatingSelector,
|
||||||
userByNameSelector,
|
usernameSelector,
|
||||||
usernameSelector
|
createUserByNameSelector,
|
||||||
|
isSignedInSelector
|
||||||
} from '../redux/selectors';
|
} from '../redux/selectors';
|
||||||
import { UserFetchState, User } from '../redux/prop-types';
|
import type { UserFetchState, User } from '../redux/prop-types';
|
||||||
import { liveCerts } from '../../config/cert-and-project-map';
|
import { liveCerts } from '../../config/cert-and-project-map';
|
||||||
import {
|
import {
|
||||||
certificateMissingErrorMessage,
|
certificateMissingErrorMessage,
|
||||||
@@ -67,6 +68,7 @@ interface ShowCertificationProps {
|
|||||||
};
|
};
|
||||||
isDonating: boolean;
|
isDonating: boolean;
|
||||||
isValidCert: boolean;
|
isValidCert: boolean;
|
||||||
|
isSignedIn: boolean;
|
||||||
location: {
|
location: {
|
||||||
pathname: string;
|
pathname: string;
|
||||||
};
|
};
|
||||||
@@ -78,41 +80,48 @@ interface ShowCertificationProps {
|
|||||||
certSlug: string;
|
certSlug: string;
|
||||||
}) => void;
|
}) => void;
|
||||||
signedInUserName: string;
|
signedInUserName: string;
|
||||||
user: User;
|
user: User | null;
|
||||||
userFetchState: UserFetchState;
|
userFetchState: UserFetchState;
|
||||||
userFullName: string;
|
userFullName: string;
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestedUserSelector = (state: unknown, { username = '' }) =>
|
|
||||||
userByNameSelector(username.toLowerCase())(state) as User;
|
|
||||||
|
|
||||||
const mapStateToProps = (state: unknown, props: ShowCertificationProps) => {
|
const mapStateToProps = (state: unknown, props: ShowCertificationProps) => {
|
||||||
const isValidCert = liveCerts.some(
|
const isValidCert = liveCerts.some(
|
||||||
({ certSlug }) => String(certSlug) === props.certSlug
|
({ certSlug }) => String(certSlug) === props.certSlug
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { username } = props;
|
||||||
|
|
||||||
|
const userByNameSelector = createUserByNameSelector(username) as (
|
||||||
|
state: unknown
|
||||||
|
) => User | null;
|
||||||
|
|
||||||
return createSelector(
|
return createSelector(
|
||||||
showCertSelector,
|
showCertSelector,
|
||||||
showCertFetchStateSelector,
|
showCertFetchStateSelector,
|
||||||
usernameSelector,
|
usernameSelector,
|
||||||
|
userByNameSelector,
|
||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
isDonatingSelector,
|
isDonatingSelector,
|
||||||
requestedUserSelector,
|
isSignedInSelector,
|
||||||
(
|
(
|
||||||
cert: Cert,
|
cert: Cert,
|
||||||
fetchState: ShowCertificationProps['fetchState'],
|
fetchState: ShowCertificationProps['fetchState'],
|
||||||
signedInUserName: string,
|
signedInUserName: string,
|
||||||
|
user: User | null,
|
||||||
userFetchState: UserFetchState,
|
userFetchState: UserFetchState,
|
||||||
isDonating: boolean,
|
isDonating: boolean,
|
||||||
user: User
|
isSignedIn: boolean
|
||||||
) => ({
|
) => ({
|
||||||
cert,
|
cert,
|
||||||
fetchState,
|
fetchState,
|
||||||
isValidCert,
|
isValidCert,
|
||||||
signedInUserName,
|
signedInUserName,
|
||||||
|
user,
|
||||||
userFetchState,
|
userFetchState,
|
||||||
isDonating,
|
isDonating,
|
||||||
user
|
isSignedIn
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -293,24 +302,19 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
|
|||||||
userFetchState: { complete: userComplete },
|
userFetchState: { complete: userComplete },
|
||||||
signedInUserName,
|
signedInUserName,
|
||||||
isDonating,
|
isDonating,
|
||||||
|
isSignedIn,
|
||||||
cert: { username = '' },
|
cert: { username = '' },
|
||||||
fetchProfileForUser,
|
fetchProfileForUser,
|
||||||
user
|
user
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (!signedInUserName || signedInUserName !== username) {
|
const isSessionUser = isSignedIn && signedInUserName === username;
|
||||||
if (isEmpty(user) && username) {
|
|
||||||
fetchProfileForUser(username);
|
if (isEmpty(user) && username) {
|
||||||
}
|
fetchProfileForUser(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!isDonationDisplayed && userComplete && isSessionUser && !isDonating) {
|
||||||
!isDonationDisplayed &&
|
|
||||||
userComplete &&
|
|
||||||
signedInUserName &&
|
|
||||||
signedInUserName === username &&
|
|
||||||
!isDonating
|
|
||||||
) {
|
|
||||||
setIsDonationDisplayed(true);
|
setIsDonationDisplayed(true);
|
||||||
callGA({
|
callGA({
|
||||||
event: 'donation_view',
|
event: 'donation_view',
|
||||||
@@ -341,7 +345,8 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
|
|||||||
isValidCert,
|
isValidCert,
|
||||||
createFlashMessage,
|
createFlashMessage,
|
||||||
signedInUserName,
|
signedInUserName,
|
||||||
location: { pathname }
|
location: { pathname },
|
||||||
|
user
|
||||||
} = props;
|
} = props;
|
||||||
const { pending, complete, errored } = fetchState;
|
const { pending, complete, errored } = fetchState;
|
||||||
|
|
||||||
@@ -359,7 +364,7 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
|
|||||||
return <RedirectHome />;
|
return <RedirectHome />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pending) {
|
if (pending || !user) {
|
||||||
return <Loader fullScreen={true} />;
|
return <Loader fullScreen={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,8 +381,6 @@ const ShowCertification = (props: ShowCertificationProps): JSX.Element => {
|
|||||||
completionTime
|
completionTime
|
||||||
} = cert;
|
} = cert;
|
||||||
|
|
||||||
const { user } = props;
|
|
||||||
|
|
||||||
const displayName = userFullName ?? username;
|
const displayName = userFullName ?? username;
|
||||||
|
|
||||||
const certDate = new Date(date);
|
const certDate = new Date(date);
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import Loader from '../components/helpers/loader';
|
|||||||
import Profile from '../components/profile/profile';
|
import Profile from '../components/profile/profile';
|
||||||
import { fetchProfileForUser } from '../redux/actions';
|
import { fetchProfileForUser } from '../redux/actions';
|
||||||
import {
|
import {
|
||||||
usernameSelector,
|
userSelector,
|
||||||
userByNameSelector,
|
userProfileFetchStateSelector,
|
||||||
userProfileFetchStateSelector
|
createUserByNameSelector
|
||||||
} from '../redux/selectors';
|
} from '../redux/selectors';
|
||||||
import { User } from '../redux/prop-types';
|
import type { User } from '../redux/prop-types';
|
||||||
import { Socials } from '../components/profile/components/internet';
|
import { Socials } from '../components/profile/components/internet';
|
||||||
|
|
||||||
interface ShowProfileOrFourOhFourProps {
|
interface ShowProfileOrFourOhFourProps {
|
||||||
@@ -20,38 +20,31 @@ interface ShowProfileOrFourOhFourProps {
|
|||||||
updateMyPortfolio: () => void;
|
updateMyPortfolio: () => void;
|
||||||
submitNewAbout: () => void;
|
submitNewAbout: () => void;
|
||||||
updateMySocials: (formValues: Socials) => void;
|
updateMySocials: (formValues: Socials) => void;
|
||||||
fetchState: {
|
|
||||||
pending: boolean;
|
|
||||||
complete: boolean;
|
|
||||||
errored: boolean;
|
|
||||||
};
|
|
||||||
isSessionUser: boolean;
|
isSessionUser: boolean;
|
||||||
maybeUser?: string;
|
maybeUser?: string;
|
||||||
requestedUser: User;
|
requestedUser: User | null;
|
||||||
showLoading: boolean;
|
showLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createRequestedUserSelector =
|
|
||||||
() =>
|
|
||||||
(state: unknown, { maybeUser = '' }) =>
|
|
||||||
userByNameSelector(maybeUser.toLowerCase())(state) as User;
|
|
||||||
const createIsSessionUserSelector =
|
|
||||||
() =>
|
|
||||||
(state: unknown, { maybeUser = '' }) =>
|
|
||||||
maybeUser.toLowerCase() === usernameSelector(state);
|
|
||||||
|
|
||||||
const makeMapStateToProps =
|
const makeMapStateToProps =
|
||||||
() => (state: unknown, props: ShowProfileOrFourOhFourProps) => {
|
() =>
|
||||||
const requestedUserSelector = createRequestedUserSelector();
|
(state: unknown, { maybeUser = '' }) => {
|
||||||
const isSessionUserSelector = createIsSessionUserSelector();
|
const username = maybeUser.toLowerCase();
|
||||||
const fetchState = userProfileFetchStateSelector(
|
const requestedUser = (
|
||||||
state
|
createUserByNameSelector as (
|
||||||
) as ShowProfileOrFourOhFourProps['fetchState'];
|
maybeUser: string
|
||||||
|
) => (state: unknown) => User | null
|
||||||
|
)(username)(state);
|
||||||
|
const sessionUser = userSelector(state) as User | null;
|
||||||
|
const isSessionUser = username === sessionUser?.username;
|
||||||
|
const fetchState = userProfileFetchStateSelector(state) as {
|
||||||
|
pending: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requestedUser: requestedUserSelector(state, props),
|
requestedUser,
|
||||||
isSessionUser: isSessionUserSelector(state, props),
|
isSessionUser,
|
||||||
showLoading: fetchState.pending,
|
showLoading: fetchState.pending
|
||||||
fetchState
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,10 +63,8 @@ function ShowProfileOrFourOhFour({
|
|||||||
}: ShowProfileOrFourOhFourProps) {
|
}: ShowProfileOrFourOhFourProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If the user is not already in the store, fetch it
|
// If the user is not already in the store, fetch it
|
||||||
if (isEmpty(requestedUser)) {
|
if (isEmpty(requestedUser) && maybeUser) {
|
||||||
if (maybeUser) {
|
fetchProfileForUser(maybeUser);
|
||||||
fetchProfileForUser(maybeUser);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
userTokenSelector
|
userTokenSelector
|
||||||
} from '../redux/selectors';
|
} from '../redux/selectors';
|
||||||
import { User } from '../redux/prop-types';
|
import type { User } from '../redux/prop-types';
|
||||||
import {
|
import {
|
||||||
submitNewAbout,
|
submitNewAbout,
|
||||||
updateMyHonesty,
|
updateMyHonesty,
|
||||||
@@ -50,7 +50,7 @@ type ShowSettingsProps = {
|
|||||||
toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void;
|
toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void;
|
||||||
updateIsHonest: () => void;
|
updateIsHonest: () => void;
|
||||||
updateQuincyEmail: (isSendQuincyEmail: boolean) => void;
|
updateQuincyEmail: (isSendQuincyEmail: boolean) => void;
|
||||||
user: User;
|
user: User | null;
|
||||||
verifyCert: typeof verifyCert;
|
verifyCert: typeof verifyCert;
|
||||||
path?: string;
|
path?: string;
|
||||||
userToken: string | null;
|
userToken: string | null;
|
||||||
@@ -61,7 +61,12 @@ const mapStateToProps = createSelector(
|
|||||||
userSelector,
|
userSelector,
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
userTokenSelector,
|
userTokenSelector,
|
||||||
(showLoading: boolean, user: User, isSignedIn, userToken: string | null) => ({
|
(
|
||||||
|
showLoading: boolean,
|
||||||
|
user: User | null,
|
||||||
|
isSignedIn,
|
||||||
|
userToken: string | null
|
||||||
|
) => ({
|
||||||
showLoading,
|
showLoading,
|
||||||
user,
|
user,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
@@ -91,34 +96,7 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
|||||||
toggleSoundMode,
|
toggleSoundMode,
|
||||||
toggleKeyboardShortcuts,
|
toggleKeyboardShortcuts,
|
||||||
resetEditorLayout,
|
resetEditorLayout,
|
||||||
user: {
|
user,
|
||||||
completedChallenges,
|
|
||||||
email,
|
|
||||||
is2018DataVisCert,
|
|
||||||
isApisMicroservicesCert,
|
|
||||||
isJsAlgoDataStructCert,
|
|
||||||
isBackEndCert,
|
|
||||||
isDataVisCert,
|
|
||||||
isFrontEndCert,
|
|
||||||
isInfosecQaCert,
|
|
||||||
isQaCertV7,
|
|
||||||
isInfosecCertV7,
|
|
||||||
isFrontEndLibsCert,
|
|
||||||
isFullStackCert,
|
|
||||||
isRespWebDesignCert,
|
|
||||||
isSciCompPyCertV7,
|
|
||||||
isDataAnalysisPyCertV7,
|
|
||||||
isMachineLearningPyCertV7,
|
|
||||||
isRelationalDatabaseCertV8,
|
|
||||||
isCollegeAlgebraPyCertV8,
|
|
||||||
isFoundationalCSharpCertV8,
|
|
||||||
isJsAlgoDataStructCertV8,
|
|
||||||
isEmailVerified,
|
|
||||||
isHonest,
|
|
||||||
sendQuincyEmail,
|
|
||||||
username,
|
|
||||||
keyboardShortcuts
|
|
||||||
},
|
|
||||||
navigate,
|
navigate,
|
||||||
showLoading,
|
showLoading,
|
||||||
updateQuincyEmail,
|
updateQuincyEmail,
|
||||||
@@ -126,11 +104,12 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
|||||||
verifyCert,
|
verifyCert,
|
||||||
userToken
|
userToken
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isSignedInRef = useRef(isSignedIn);
|
const isSignedInRef = useRef(isSignedIn);
|
||||||
|
|
||||||
const examTokenFlag = useFeatureIsOn('exam-token-widget');
|
const examTokenFlag = useFeatureIsOn('exam-token-widget');
|
||||||
|
|
||||||
if (showLoading) {
|
if (showLoading || !user) {
|
||||||
return <Loader fullScreen={true} />;
|
return <Loader fullScreen={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +117,36 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
|||||||
navigate(`${apiLocation}/signin`);
|
navigate(`${apiLocation}/signin`);
|
||||||
return <Loader fullScreen={true} />;
|
return <Loader fullScreen={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
completedChallenges,
|
||||||
|
email,
|
||||||
|
is2018DataVisCert,
|
||||||
|
isApisMicroservicesCert,
|
||||||
|
isJsAlgoDataStructCert,
|
||||||
|
isBackEndCert,
|
||||||
|
isDataVisCert,
|
||||||
|
isFrontEndCert,
|
||||||
|
isInfosecQaCert,
|
||||||
|
isQaCertV7,
|
||||||
|
isInfosecCertV7,
|
||||||
|
isFrontEndLibsCert,
|
||||||
|
isFullStackCert,
|
||||||
|
isRespWebDesignCert,
|
||||||
|
isSciCompPyCertV7,
|
||||||
|
isDataAnalysisPyCertV7,
|
||||||
|
isMachineLearningPyCertV7,
|
||||||
|
isRelationalDatabaseCertV8,
|
||||||
|
isCollegeAlgebraPyCertV8,
|
||||||
|
isFoundationalCSharpCertV8,
|
||||||
|
isJsAlgoDataStructCertV8,
|
||||||
|
isEmailVerified,
|
||||||
|
isHonest,
|
||||||
|
sendQuincyEmail,
|
||||||
|
username,
|
||||||
|
keyboardShortcuts
|
||||||
|
} = user;
|
||||||
|
|
||||||
const sound = (store.get('fcc-sound') as boolean) ?? false;
|
const sound = (store.get('fcc-sound') as boolean) ?? false;
|
||||||
const editorLayout = (store.get('challenge-layout') as boolean) ?? false;
|
const editorLayout = (store.get('challenge-layout') as boolean) ?? false;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { isSignedInSelector, userSelector } from '../redux/selectors';
|
|||||||
import { hardGoTo as navigate } from '../redux/actions';
|
import { hardGoTo as navigate } from '../redux/actions';
|
||||||
import { updateMyEmail } from '../redux/settings/actions';
|
import { updateMyEmail } from '../redux/settings/actions';
|
||||||
import { maybeEmailRE } from '../utils';
|
import { maybeEmailRE } from '../utils';
|
||||||
|
import type { User } from '../redux/prop-types';
|
||||||
|
|
||||||
const { apiLocation } = envData;
|
const { apiLocation } = envData;
|
||||||
|
|
||||||
@@ -41,11 +42,8 @@ interface ShowUpdateEmailProps {
|
|||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
userSelector,
|
userSelector,
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
(
|
(user: User | null, isSignedIn) => ({
|
||||||
{ email, emailVerified }: { email: string; emailVerified: boolean },
|
isNewEmail: !user?.email || user.emailVerified,
|
||||||
isSignedIn
|
|
||||||
) => ({
|
|
||||||
isNewEmail: !email || emailVerified,
|
|
||||||
isSignedIn
|
isSignedIn
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
themeSelector
|
themeSelector
|
||||||
} from '../../redux/selectors';
|
} from '../../redux/selectors';
|
||||||
import { LocalStorageThemes, DonateFormState } from '../../redux/types';
|
import { LocalStorageThemes, DonateFormState } from '../../redux/types';
|
||||||
import type { CompletedChallenge } from '../../redux/prop-types';
|
import type { CompletedChallenge, User } from '../../redux/prop-types';
|
||||||
import { CENTS_IN_DOLLAR, formattedAmountLabel } from './utils';
|
import { CENTS_IN_DOLLAR, formattedAmountLabel } from './utils';
|
||||||
import DonateCompletion from './donate-completion';
|
import DonateCompletion from './donate-completion';
|
||||||
import PatreonButton from './patreon-button';
|
import PatreonButton from './patreon-button';
|
||||||
@@ -62,7 +62,7 @@ type PostCharge = (data: {
|
|||||||
type DonateFormProps = {
|
type DonateFormProps = {
|
||||||
postCharge: PostCharge;
|
postCharge: PostCharge;
|
||||||
defaultTheme?: LocalStorageThemes;
|
defaultTheme?: LocalStorageThemes;
|
||||||
email: string;
|
email?: string;
|
||||||
handleProcessing?: () => void;
|
handleProcessing?: () => void;
|
||||||
editAmount?: () => void;
|
editAmount?: () => void;
|
||||||
selectedDonationAmount?: DonationAmount;
|
selectedDonationAmount?: DonationAmount;
|
||||||
@@ -91,7 +91,7 @@ const mapStateToProps = createSelector(
|
|||||||
isSignedIn: DonateFormProps['isSignedIn'],
|
isSignedIn: DonateFormProps['isSignedIn'],
|
||||||
isDonating: DonateFormProps['isDonating'],
|
isDonating: DonateFormProps['isDonating'],
|
||||||
donationFormState: DonateFormState,
|
donationFormState: DonateFormState,
|
||||||
{ email }: { email: string },
|
user: User | null,
|
||||||
completedChallenges: CompletedChallenge[],
|
completedChallenges: CompletedChallenge[],
|
||||||
theme: LocalStorageThemes
|
theme: LocalStorageThemes
|
||||||
) => ({
|
) => ({
|
||||||
@@ -99,7 +99,7 @@ const mapStateToProps = createSelector(
|
|||||||
isDonating,
|
isDonating,
|
||||||
showLoading,
|
showLoading,
|
||||||
donationFormState,
|
donationFormState,
|
||||||
email,
|
email: user?.email,
|
||||||
completedChallenges,
|
completedChallenges,
|
||||||
theme
|
theme
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import envData from '../../../config/env.json';
|
import envData from '../../../config/env.json';
|
||||||
import { userSelector, signInLoadingSelector } from '../../redux/selectors';
|
import { userSelector, signInLoadingSelector } from '../../redux/selectors';
|
||||||
import { LocalStorageThemes } from '../../redux/types';
|
import { LocalStorageThemes } from '../../redux/types';
|
||||||
|
import type { User } from '../../redux/prop-types';
|
||||||
import { DonationApprovalData, PostPayment } from './types';
|
import { DonationApprovalData, PostPayment } from './types';
|
||||||
import PayPalButtonScriptLoader from './paypal-button-script-loader';
|
import PayPalButtonScriptLoader from './paypal-button-script-loader';
|
||||||
|
|
||||||
@@ -177,8 +178,8 @@ class PaypalButton extends Component<PaypalButtonProps, PaypalButtonState> {
|
|||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
userSelector,
|
userSelector,
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
({ isDonating }: { isDonating: boolean }, showLoading: boolean) => ({
|
(user: User | null, showLoading: boolean) => ({
|
||||||
isDonating,
|
isDonating: !!user?.isDonating,
|
||||||
showLoading
|
showLoading
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import LearnAlert from './learn-alert';
|
|||||||
|
|
||||||
interface IntroProps {
|
interface IntroProps {
|
||||||
complete?: boolean;
|
complete?: boolean;
|
||||||
completedChallengeCount?: number;
|
completedChallengeCount: number;
|
||||||
isSignedIn?: boolean;
|
isSignedIn?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
pending?: boolean;
|
pending?: boolean;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ describe('<Intro />', () => {
|
|||||||
|
|
||||||
const loggedInProps = {
|
const loggedInProps = {
|
||||||
complete: true,
|
complete: true,
|
||||||
|
completedChallengeCount: 0,
|
||||||
isSignedIn: true,
|
isSignedIn: true,
|
||||||
name: 'Development User',
|
name: 'Development User',
|
||||||
navigate: () => jest.fn(),
|
navigate: () => jest.fn(),
|
||||||
@@ -39,6 +40,7 @@ const loggedInProps = {
|
|||||||
|
|
||||||
const loggedOutProps = {
|
const loggedOutProps = {
|
||||||
complete: true,
|
complete: true,
|
||||||
|
completedChallengeCount: 0,
|
||||||
isSignedIn: false,
|
isSignedIn: false,
|
||||||
name: '',
|
name: '',
|
||||||
navigate: () => jest.fn(),
|
navigate: () => jest.fn(),
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { Spacer } from '@freecodecamp/ui';
|
import { Spacer } from '@freecodecamp/ui';
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -17,13 +15,6 @@ import { ButtonLink } from '../helpers';
|
|||||||
import { showUpcomingChanges } from '../../../config/env.json';
|
import { showUpcomingChanges } from '../../../config/env.json';
|
||||||
|
|
||||||
import './map.css';
|
import './map.css';
|
||||||
|
|
||||||
import {
|
|
||||||
isSignedInSelector,
|
|
||||||
currentCertsSelector,
|
|
||||||
completedChallengesIdsSelector
|
|
||||||
} from '../../redux/selectors';
|
|
||||||
|
|
||||||
interface MapProps {
|
interface MapProps {
|
||||||
forLanding?: boolean;
|
forLanding?: boolean;
|
||||||
}
|
}
|
||||||
@@ -46,17 +37,6 @@ const superBlockHeadings: { [key in SuperBlockStage]: string } = {
|
|||||||
[SuperBlockStage.Catalog]: 'landing.catalog-heading'
|
[SuperBlockStage.Catalog]: 'landing.catalog-heading'
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
|
||||||
isSignedInSelector,
|
|
||||||
currentCertsSelector,
|
|
||||||
completedChallengesIdsSelector,
|
|
||||||
(isSignedIn: boolean, currentCerts, completedChallengeIds: string[]) => ({
|
|
||||||
isSignedIn,
|
|
||||||
currentCerts,
|
|
||||||
completedChallengeIds
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
function MapLi({
|
function MapLi({
|
||||||
superBlock,
|
superBlock,
|
||||||
landing = false
|
landing = false
|
||||||
@@ -124,4 +104,4 @@ function Map({ forLanding = false }: MapProps) {
|
|||||||
|
|
||||||
Map.displayName = 'Map';
|
Map.displayName = 'Map';
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Map);
|
export default Map;
|
||||||
|
|||||||
@@ -6,14 +6,10 @@ import {
|
|||||||
} from '@growthbook/growthbook-react';
|
} from '@growthbook/growthbook-react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import {
|
import { userSelector, userFetchStateSelector } from '../../redux/selectors';
|
||||||
isSignedInSelector,
|
|
||||||
userSelector,
|
|
||||||
userFetchStateSelector
|
|
||||||
} from '../../redux/selectors';
|
|
||||||
import envData from '../../../config/env.json';
|
import envData from '../../../config/env.json';
|
||||||
import defaultGrowthBookFeatures from '../../../config/growthbook-features-default.json';
|
import defaultGrowthBookFeatures from '../../../config/growthbook-features-default.json';
|
||||||
import { User, UserFetchState } from '../../redux/prop-types';
|
import type { User, UserFetchState } from '../../redux/prop-types';
|
||||||
import { getUUID } from '../../utils/growthbook-cookie';
|
import { getUUID } from '../../utils/growthbook-cookie';
|
||||||
import callGA from '../../analytics/call-ga';
|
import callGA from '../../analytics/call-ga';
|
||||||
import GrowthBookReduxConnector from './growth-book-redux-connector';
|
import GrowthBookReduxConnector from './growth-book-redux-connector';
|
||||||
@@ -30,11 +26,9 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
isSignedInSelector,
|
|
||||||
userSelector,
|
userSelector,
|
||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
(isSignedIn, user: User, userFetchState: UserFetchState) => ({
|
(user: User | null, userFetchState: UserFetchState) => ({
|
||||||
isSignedIn,
|
|
||||||
user,
|
user,
|
||||||
userFetchState
|
userFetchState
|
||||||
})
|
})
|
||||||
@@ -56,7 +50,6 @@ interface UserAttributes {
|
|||||||
|
|
||||||
const GrowthBookWrapper = ({
|
const GrowthBookWrapper = ({
|
||||||
children,
|
children,
|
||||||
isSignedIn,
|
|
||||||
user,
|
user,
|
||||||
userFetchState
|
userFetchState
|
||||||
}: GrowthBookWrapper) => {
|
}: GrowthBookWrapper) => {
|
||||||
@@ -105,7 +98,7 @@ const GrowthBookWrapper = ({
|
|||||||
clientLocal: clientLocale
|
clientLocal: clientLocale
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSignedIn) {
|
if (user) {
|
||||||
userAttributes = {
|
userAttributes = {
|
||||||
...userAttributes,
|
...userAttributes,
|
||||||
staff: user.email.includes('@freecodecamp'),
|
staff: user.email.includes('@freecodecamp'),
|
||||||
@@ -116,7 +109,7 @@ const GrowthBookWrapper = ({
|
|||||||
}
|
}
|
||||||
growthbook.setAttributes(userAttributes);
|
growthbook.setAttributes(userAttributes);
|
||||||
}
|
}
|
||||||
}, [isSignedIn, user, userFetchState, growthbook]);
|
}, [user, userFetchState, growthbook]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GrowthBookProvider growthbook={growthbook}>
|
<GrowthBookProvider growthbook={growthbook}>
|
||||||
|
|||||||
@@ -1,42 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { Spacer } from '@freecodecamp/ui';
|
import { Spacer } from '@freecodecamp/ui';
|
||||||
|
|
||||||
import { certificatesByNameSelector } from '../../../redux/selectors';
|
import type { CurrentCert, User } from '../../../redux/prop-types';
|
||||||
import type { CurrentCert } from '../../../redux/prop-types';
|
|
||||||
import { FullWidthRow, ButtonLink } from '../../helpers';
|
import { FullWidthRow, ButtonLink } from '../../helpers';
|
||||||
|
import { getCertifications } from './utils/certification';
|
||||||
|
|
||||||
import './certifications.css';
|
import './certifications.css';
|
||||||
|
|
||||||
const mapStateToProps = (
|
|
||||||
state: Record<string, unknown>,
|
|
||||||
props: CertificationProps
|
|
||||||
) =>
|
|
||||||
createSelector(
|
|
||||||
certificatesByNameSelector(props.username.toLowerCase()),
|
|
||||||
({
|
|
||||||
hasModernCert,
|
|
||||||
hasLegacyCert,
|
|
||||||
currentCerts,
|
|
||||||
legacyCerts
|
|
||||||
}: Pick<
|
|
||||||
CertificationProps,
|
|
||||||
'hasModernCert' | 'hasLegacyCert' | 'currentCerts' | 'legacyCerts'
|
|
||||||
>) => ({
|
|
||||||
hasModernCert,
|
|
||||||
hasLegacyCert,
|
|
||||||
currentCerts,
|
|
||||||
legacyCerts
|
|
||||||
})
|
|
||||||
)(state);
|
|
||||||
|
|
||||||
interface CertificationProps {
|
interface CertificationProps {
|
||||||
currentCerts?: CurrentCert[];
|
user: User;
|
||||||
hasLegacyCert?: boolean;
|
|
||||||
hasModernCert?: boolean;
|
|
||||||
legacyCerts?: CurrentCert[];
|
|
||||||
username: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CertButtonProps {
|
interface CertButtonProps {
|
||||||
@@ -62,13 +35,12 @@ function CertButton({ username, cert }: CertButtonProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Certificates({
|
function Certificates({ user }: CertificationProps): JSX.Element {
|
||||||
currentCerts,
|
const { username } = user;
|
||||||
legacyCerts,
|
|
||||||
hasLegacyCert,
|
const { currentCerts, legacyCerts, hasLegacyCert, hasModernCert } =
|
||||||
hasModernCert,
|
getCertifications(user);
|
||||||
username
|
|
||||||
}: CertificationProps): JSX.Element {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<FullWidthRow className='profile-certifications'>
|
<FullWidthRow className='profile-certifications'>
|
||||||
@@ -122,4 +94,4 @@ function Certificates({
|
|||||||
|
|
||||||
Certificates.displayName = 'Certifications';
|
Certificates.displayName = 'Certifications';
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Certificates);
|
export default Certificates;
|
||||||
|
|||||||
152
client/src/components/profile/components/utils/certification.ts
Normal file
152
client/src/components/profile/components/utils/certification.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { Certification } from '../../../../../../shared/config/certification-settings';
|
||||||
|
import { User } from '../../../../redux/prop-types';
|
||||||
|
|
||||||
|
export const getCertifications = (user: User) => {
|
||||||
|
const {
|
||||||
|
isRespWebDesignCert,
|
||||||
|
is2018DataVisCert,
|
||||||
|
isFrontEndLibsCert,
|
||||||
|
isJsAlgoDataStructCert,
|
||||||
|
isApisMicroservicesCert,
|
||||||
|
isInfosecQaCert,
|
||||||
|
isQaCertV7,
|
||||||
|
isInfosecCertV7,
|
||||||
|
isFrontEndCert,
|
||||||
|
isBackEndCert,
|
||||||
|
isDataVisCert,
|
||||||
|
isFullStackCert,
|
||||||
|
isSciCompPyCertV7,
|
||||||
|
isDataAnalysisPyCertV7,
|
||||||
|
isMachineLearningPyCertV7,
|
||||||
|
isRelationalDatabaseCertV8,
|
||||||
|
isCollegeAlgebraPyCertV8,
|
||||||
|
isFoundationalCSharpCertV8,
|
||||||
|
isJsAlgoDataStructCertV8
|
||||||
|
} = user;
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasModernCert:
|
||||||
|
isRespWebDesignCert ||
|
||||||
|
is2018DataVisCert ||
|
||||||
|
isFrontEndLibsCert ||
|
||||||
|
isApisMicroservicesCert ||
|
||||||
|
isQaCertV7 ||
|
||||||
|
isInfosecCertV7 ||
|
||||||
|
isFullStackCert ||
|
||||||
|
isSciCompPyCertV7 ||
|
||||||
|
isDataAnalysisPyCertV7 ||
|
||||||
|
isMachineLearningPyCertV7 ||
|
||||||
|
isRelationalDatabaseCertV8 ||
|
||||||
|
isCollegeAlgebraPyCertV8 ||
|
||||||
|
isFoundationalCSharpCertV8 ||
|
||||||
|
isJsAlgoDataStructCertV8,
|
||||||
|
hasLegacyCert:
|
||||||
|
isFrontEndCert ||
|
||||||
|
isJsAlgoDataStructCert ||
|
||||||
|
isBackEndCert ||
|
||||||
|
isDataVisCert ||
|
||||||
|
isInfosecQaCert,
|
||||||
|
isFullStackCert,
|
||||||
|
currentCerts: [
|
||||||
|
{
|
||||||
|
show: isRespWebDesignCert,
|
||||||
|
title: 'Responsive Web Design Certification',
|
||||||
|
certSlug: Certification.RespWebDesign
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isJsAlgoDataStructCertV8,
|
||||||
|
title: 'JavaScript Algorithms and Data Structures Certification',
|
||||||
|
certSlug: Certification.JsAlgoDataStructNew
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isFrontEndLibsCert,
|
||||||
|
title: 'Front End Development Libraries Certification',
|
||||||
|
certSlug: Certification.FrontEndDevLibs
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: is2018DataVisCert,
|
||||||
|
title: 'Data Visualization Certification',
|
||||||
|
certSlug: Certification.DataVis
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isRelationalDatabaseCertV8,
|
||||||
|
title: 'Relational Database Certification',
|
||||||
|
certSlug: Certification.RelationalDb
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isApisMicroservicesCert,
|
||||||
|
title: 'Back End Development and APIs Certification',
|
||||||
|
certSlug: Certification.BackEndDevApis
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isQaCertV7,
|
||||||
|
title: 'Quality Assurance Certification',
|
||||||
|
certSlug: Certification.QualityAssurance
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isSciCompPyCertV7,
|
||||||
|
title: 'Scientific Computing with Python Certification',
|
||||||
|
certSlug: Certification.SciCompPy
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isDataAnalysisPyCertV7,
|
||||||
|
title: 'Data Analysis with Python Certification',
|
||||||
|
certSlug: Certification.DataAnalysisPy
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isInfosecCertV7,
|
||||||
|
title: 'Information Security Certification',
|
||||||
|
certSlug: Certification.InfoSec
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isMachineLearningPyCertV7,
|
||||||
|
title: 'Machine Learning with Python Certification',
|
||||||
|
certSlug: Certification.MachineLearningPy
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isCollegeAlgebraPyCertV8,
|
||||||
|
title: 'College Algebra with Python Certification',
|
||||||
|
certSlug: Certification.CollegeAlgebraPy
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isFoundationalCSharpCertV8,
|
||||||
|
title: 'Foundational C# with Microsoft Certification',
|
||||||
|
certSlug: Certification.FoundationalCSharp
|
||||||
|
}
|
||||||
|
],
|
||||||
|
legacyCerts: [
|
||||||
|
{
|
||||||
|
show: isFrontEndCert,
|
||||||
|
title: 'Front End Certification',
|
||||||
|
certSlug: Certification.LegacyFrontEnd
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isJsAlgoDataStructCert,
|
||||||
|
title: 'Legacy JavaScript Algorithms and Data Structures Certification',
|
||||||
|
certSlug: Certification.JsAlgoDataStruct
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isBackEndCert,
|
||||||
|
title: 'Back End Certification',
|
||||||
|
certSlug: Certification.LegacyBackEnd
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isDataVisCert,
|
||||||
|
title: 'Data Visualization Certification',
|
||||||
|
certSlug: Certification.LegacyDataVis
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isInfosecQaCert,
|
||||||
|
title: 'Information Security and Quality Assurance Certification',
|
||||||
|
// Keep the current public profile cert slug
|
||||||
|
certSlug: Certification.LegacyInfoSecQa
|
||||||
|
},
|
||||||
|
{
|
||||||
|
show: isFullStackCert,
|
||||||
|
title: 'Full Stack Certification',
|
||||||
|
// Keep the current public profile cert slug
|
||||||
|
certSlug: Certification.LegacyFullStack
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -81,7 +81,7 @@ const notMyProfileProps = {
|
|||||||
};
|
};
|
||||||
function reducer() {
|
function reducer() {
|
||||||
return {
|
return {
|
||||||
app: { appUsername: 'vasili', user: { vasili: userProps.user } }
|
app: { user: { sessionUser: userProps.user } }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function renderWithRedux(ui: JSX.Element) {
|
function renderWithRedux(ui: JSX.Element) {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ function UserProfile({ user, isSessionUser }: ProfileProps): JSX.Element {
|
|||||||
{showPortfolio ? (
|
{showPortfolio ? (
|
||||||
<PortfolioProjects portfolioProjects={portfolio} />
|
<PortfolioProjects portfolioProjects={portfolio} />
|
||||||
) : null}
|
) : null}
|
||||||
{showCerts ? <Certifications username={username} /> : null}
|
{showCerts ? <Certifications user={user} /> : null}
|
||||||
{showTimeLine ? (
|
{showTimeLine ? (
|
||||||
<Timeline completedMap={completedChallenges} username={username} />
|
<Timeline completedMap={completedChallenges} username={username} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
userSelector,
|
userSelector,
|
||||||
isSignedInSelector
|
isSignedInSelector
|
||||||
} from '../redux/selectors';
|
} from '../redux/selectors';
|
||||||
|
import type { User } from '../redux/prop-types';
|
||||||
|
|
||||||
import './email-sign-up.css';
|
import './email-sign-up.css';
|
||||||
interface AcceptPrivacyTermsProps {
|
interface AcceptPrivacyTermsProps {
|
||||||
@@ -24,25 +25,18 @@ interface AcceptPrivacyTermsProps {
|
|||||||
acceptedPrivacyTerms: boolean;
|
acceptedPrivacyTerms: boolean;
|
||||||
isSignedIn: boolean;
|
isSignedIn: boolean;
|
||||||
showLoading: boolean;
|
showLoading: boolean;
|
||||||
completedChallengeCount?: number;
|
completedChallengeCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
userSelector,
|
userSelector,
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
(
|
(user: User | null, isSignedIn: boolean, showLoading: boolean) => ({
|
||||||
{
|
acceptedPrivacyTerms: !!user?.acceptedPrivacyTerms,
|
||||||
acceptedPrivacyTerms,
|
|
||||||
completedChallengeCount
|
|
||||||
}: { acceptedPrivacyTerms: boolean; completedChallengeCount: number },
|
|
||||||
isSignedIn: boolean,
|
|
||||||
showLoading: boolean
|
|
||||||
) => ({
|
|
||||||
acceptedPrivacyTerms,
|
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
showLoading,
|
showLoading,
|
||||||
completedChallengeCount
|
completedChallengeCount: user?.completedChallengeCount ?? 0
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
@@ -109,7 +103,7 @@ function AcceptPrivacyTerms({
|
|||||||
acceptedPrivacyTerms,
|
acceptedPrivacyTerms,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
showLoading,
|
showLoading,
|
||||||
completedChallengeCount = 0
|
completedChallengeCount
|
||||||
}: AcceptPrivacyTermsProps) {
|
}: AcceptPrivacyTermsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const acceptedPrivacyRef = useRef(acceptedPrivacyTerms);
|
const acceptedPrivacyRef = useRef(acceptedPrivacyTerms);
|
||||||
|
|||||||
@@ -23,18 +23,18 @@ interface FetchState {
|
|||||||
errored: boolean;
|
errored: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface User {
|
type MaybeUser = {
|
||||||
name: string;
|
name: string;
|
||||||
username: string;
|
username: string;
|
||||||
completedChallengeCount: number;
|
completedChallengeCount: number;
|
||||||
isDonating: boolean;
|
isDonating: boolean;
|
||||||
}
|
} | null;
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
(fetchState: FetchState, isSignedIn: boolean, user: User) => ({
|
(fetchState: FetchState, isSignedIn: boolean, user: MaybeUser) => ({
|
||||||
fetchState,
|
fetchState,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
user
|
user
|
||||||
@@ -49,7 +49,7 @@ interface LearnPageProps {
|
|||||||
isSignedIn: boolean;
|
isSignedIn: boolean;
|
||||||
fetchState: FetchState;
|
fetchState: FetchState;
|
||||||
state: Record<string, unknown>;
|
state: Record<string, unknown>;
|
||||||
user: User;
|
user: MaybeUser;
|
||||||
data: {
|
data: {
|
||||||
challengeNode: {
|
challengeNode: {
|
||||||
challenge: {
|
challenge: {
|
||||||
@@ -59,10 +59,12 @@ interface LearnPageProps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EMPTY_USER = { name: '', completedChallengeCount: 0, isDonating: false };
|
||||||
|
|
||||||
function LearnPage({
|
function LearnPage({
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
fetchState: { pending, complete },
|
fetchState: { pending, complete },
|
||||||
user: { name = '', completedChallengeCount = 0, isDonating = false },
|
user,
|
||||||
data: {
|
data: {
|
||||||
challengeNode: {
|
challengeNode: {
|
||||||
challenge: {
|
challenge: {
|
||||||
@@ -71,6 +73,8 @@ function LearnPage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}: LearnPageProps) {
|
}: LearnPageProps) {
|
||||||
|
const { name, completedChallengeCount, isDonating } = user ?? EMPTY_USER;
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onLearnDonationAlertClick = () => {
|
const onLearnDonationAlertClick = () => {
|
||||||
|
|||||||
@@ -49,9 +49,8 @@ const analyticsDataMock = {
|
|||||||
|
|
||||||
const signedInStoreMock = {
|
const signedInStoreMock = {
|
||||||
app: {
|
app: {
|
||||||
appUsername: 'devuser',
|
|
||||||
user: {
|
user: {
|
||||||
devuser: {
|
sessionUser: {
|
||||||
completedChallenges: [
|
completedChallenges: [
|
||||||
{
|
{
|
||||||
id: 'bd7123c8c441eddfaeb5bdef',
|
id: 'bd7123c8c441eddfaeb5bdef',
|
||||||
@@ -81,11 +80,8 @@ const signedInStoreMock = {
|
|||||||
|
|
||||||
const signedOutStoreMock = {
|
const signedOutStoreMock = {
|
||||||
app: {
|
app: {
|
||||||
appUsername: '',
|
|
||||||
user: {
|
user: {
|
||||||
'': {
|
sessionUser: null
|
||||||
completedChallenges: []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -162,11 +158,8 @@ describe('donation-saga', () => {
|
|||||||
|
|
||||||
const signedOutStoreMock = {
|
const signedOutStoreMock = {
|
||||||
app: {
|
app: {
|
||||||
appUsername: '',
|
|
||||||
user: {
|
user: {
|
||||||
'': {
|
sessionUser: null
|
||||||
completedChallenges: []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const initialState = {
|
|||||||
app: {
|
app: {
|
||||||
isOnline: true,
|
isOnline: true,
|
||||||
isServerOnline: true,
|
isServerOnline: true,
|
||||||
appUsername: 'developmentuser'
|
user: { sessionUser: {} }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,9 @@ import {
|
|||||||
|
|
||||||
function* fetchSessionUser() {
|
function* fetchSessionUser() {
|
||||||
try {
|
try {
|
||||||
const {
|
const { data: user } = yield call(getSessionUser);
|
||||||
data: { user = {}, result = '' }
|
|
||||||
} = yield call(getSessionUser);
|
|
||||||
const appUser = user[result] || {};
|
|
||||||
|
|
||||||
yield put(fetchUserComplete({ user: appUser, username: result }));
|
yield put(fetchUserComplete({ user }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('failed to fetch user', e);
|
console.log('failed to fetch user', e);
|
||||||
yield put(fetchUserError(e));
|
yield put(fetchUserError(e));
|
||||||
@@ -25,13 +22,8 @@ function* fetchOtherUser({ payload: maybeUser = '' }) {
|
|||||||
try {
|
try {
|
||||||
const maybeUserLC = maybeUser.toLowerCase();
|
const maybeUserLC = maybeUser.toLowerCase();
|
||||||
|
|
||||||
const {
|
const { data: otherUser } = yield call(getUserProfile, maybeUserLC);
|
||||||
data: { entities: { user = {} } = {}, result = '' }
|
yield put(fetchProfileForUserComplete({ user: otherUser }));
|
||||||
} = yield call(getUserProfile, maybeUserLC);
|
|
||||||
const otherUser = user[result] || {};
|
|
||||||
yield put(
|
|
||||||
fetchProfileForUserComplete({ user: otherUser, username: result })
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
yield put(fetchProfileForUserError(e));
|
yield put(fetchProfileForUserError(e));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export const defaultDonationFormState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
appUsername: '',
|
|
||||||
isRandomCompletionThreshold: false,
|
isRandomCompletionThreshold: false,
|
||||||
donatableSectionRecentlyCompleted: null,
|
donatableSectionRecentlyCompleted: null,
|
||||||
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
|
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
|
||||||
@@ -61,7 +60,7 @@ const initialState = {
|
|||||||
showCertFetchState: {
|
showCertFetchState: {
|
||||||
...defaultFetchState
|
...defaultFetchState
|
||||||
},
|
},
|
||||||
user: {},
|
user: { sessionUser: null, otherUser: null },
|
||||||
userFetchState: {
|
userFetchState: {
|
||||||
...defaultFetchState
|
...defaultFetchState
|
||||||
},
|
},
|
||||||
@@ -107,8 +106,8 @@ function spreadThePayloadOnUser(state, payload) {
|
|||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[state.appUsername]: {
|
sessionUser: {
|
||||||
...state.user[state.appUsername],
|
...state.user.sessionUser,
|
||||||
...payload
|
...payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,13 +117,12 @@ function spreadThePayloadOnUser(state, payload) {
|
|||||||
export const reducer = handleActions(
|
export const reducer = handleActions(
|
||||||
{
|
{
|
||||||
[actionTypes.acceptTermsComplete]: (state, { payload }) => {
|
[actionTypes.acceptTermsComplete]: (state, { payload }) => {
|
||||||
const { appUsername } = state;
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[appUsername]: {
|
sessionUser: {
|
||||||
...state.user[appUsername],
|
...state.user.sessionUser,
|
||||||
// TODO: the user accepts the privacy terms in practice during auth
|
// TODO: the user accepts the privacy terms in practice during auth
|
||||||
// however, it's currently being used to track if they've accepted
|
// however, it's currently being used to track if they've accepted
|
||||||
// or rejected the newsletter. Ideally this should be migrated,
|
// or rejected the newsletter. Ideally this should be migrated,
|
||||||
@@ -132,7 +130,7 @@ export const reducer = handleActions(
|
|||||||
acceptedPrivacyTerms: true,
|
acceptedPrivacyTerms: true,
|
||||||
sendQuincyEmail:
|
sendQuincyEmail:
|
||||||
payload === null
|
payload === null
|
||||||
? state.user[appUsername].sendQuincyEmail
|
? state.user.sessionUser.sendQuincyEmail
|
||||||
: payload
|
: payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,13 +173,12 @@ export const reducer = handleActions(
|
|||||||
donationFormState: { ...defaultDonationFormState, processing: true }
|
donationFormState: { ...defaultDonationFormState, processing: true }
|
||||||
}),
|
}),
|
||||||
[actionTypes.postChargeComplete]: state => {
|
[actionTypes.postChargeComplete]: state => {
|
||||||
const { appUsername } = state;
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[appUsername]: {
|
sessionUser: {
|
||||||
...state.user[appUsername],
|
...state.user.sessionUser,
|
||||||
isDonating: true
|
isDonating: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -205,18 +202,14 @@ export const reducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
userProfileFetchState: { ...defaultFetchState }
|
userProfileFetchState: { ...defaultFetchState }
|
||||||
}),
|
}),
|
||||||
[actionTypes.fetchUserComplete]: (
|
[actionTypes.fetchUserComplete]: (state, { payload: { user } }) => ({
|
||||||
state,
|
|
||||||
{ payload: { user, username } }
|
|
||||||
) => ({
|
|
||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[username]: { ...user, sessionUser: true }
|
sessionUser: user
|
||||||
},
|
},
|
||||||
appUsername: username,
|
|
||||||
currentChallengeId:
|
currentChallengeId:
|
||||||
user.currentChallengeId || store.get(CURRENT_CHALLENGE_KEY),
|
user?.currentChallengeId || store.get(CURRENT_CHALLENGE_KEY),
|
||||||
userFetchState: {
|
userFetchState: {
|
||||||
pending: false,
|
pending: false,
|
||||||
complete: true,
|
complete: true,
|
||||||
@@ -235,15 +228,13 @@ export const reducer = handleActions(
|
|||||||
}),
|
}),
|
||||||
[actionTypes.fetchProfileForUserComplete]: (
|
[actionTypes.fetchProfileForUserComplete]: (
|
||||||
state,
|
state,
|
||||||
{ payload: { user, username } }
|
{ payload: { user } }
|
||||||
) => {
|
) => {
|
||||||
const previousUserObject =
|
|
||||||
username in state.user ? state.user[username] : {};
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[username]: { ...previousUserObject, ...user }
|
otherUser: user
|
||||||
},
|
},
|
||||||
userProfileFetchState: {
|
userProfileFetchState: {
|
||||||
...defaultFetchState,
|
...defaultFetchState,
|
||||||
@@ -291,8 +282,7 @@ export const reducer = handleActions(
|
|||||||
}),
|
}),
|
||||||
[actionTypes.resetUserData]: state => ({
|
[actionTypes.resetUserData]: state => ({
|
||||||
...state,
|
...state,
|
||||||
appUsername: '',
|
user: { ...state.user, sessionUser: null }
|
||||||
user: {}
|
|
||||||
}),
|
}),
|
||||||
[actionTypes.openSignoutModal]: state => ({
|
[actionTypes.openSignoutModal]: state => ({
|
||||||
...state,
|
...state,
|
||||||
@@ -335,15 +325,14 @@ export const reducer = handleActions(
|
|||||||
let submittedchallenges = [
|
let submittedchallenges = [
|
||||||
{ ...submittedChallenge, completedDate: Date.now() }
|
{ ...submittedChallenge, completedDate: Date.now() }
|
||||||
];
|
];
|
||||||
const { appUsername } = state;
|
|
||||||
|
|
||||||
return examResults && !examResults.passed
|
return examResults && !examResults.passed
|
||||||
? {
|
? {
|
||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[appUsername]: {
|
sessionUser: {
|
||||||
...state.user[appUsername],
|
...state.user.sessionUser,
|
||||||
examResults
|
examResults
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,12 +341,12 @@ export const reducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[appUsername]: {
|
sessionUser: {
|
||||||
...state.user[appUsername],
|
...state.user.sessionUser,
|
||||||
completedChallenges: uniqBy(
|
completedChallenges: uniqBy(
|
||||||
[
|
[
|
||||||
...submittedchallenges,
|
...submittedchallenges,
|
||||||
...state.user[appUsername].completedChallenges
|
...state.user.sessionUser.completedChallenges
|
||||||
],
|
],
|
||||||
'id'
|
'id'
|
||||||
),
|
),
|
||||||
@@ -369,13 +358,12 @@ export const reducer = handleActions(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
[actionTypes.setMsUsername]: (state, { payload }) => {
|
[actionTypes.setMsUsername]: (state, { payload }) => {
|
||||||
const { appUsername } = state;
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[appUsername]: {
|
sessionUser: {
|
||||||
...state.user[appUsername],
|
...state.user.sessionUser,
|
||||||
msUsername: payload
|
msUsername: payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,26 +376,24 @@ export const reducer = handleActions(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
[actionTypes.updateUserToken]: (state, { payload }) => {
|
[actionTypes.updateUserToken]: (state, { payload }) => {
|
||||||
const { appUsername } = state;
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[appUsername]: {
|
sessionUser: {
|
||||||
...state.user[appUsername],
|
...state.user.sessionUser,
|
||||||
userToken: payload
|
userToken: payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[actionTypes.deleteUserTokenComplete]: state => {
|
[actionTypes.deleteUserTokenComplete]: state => {
|
||||||
const { appUsername } = state;
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[appUsername]: {
|
sessionUser: {
|
||||||
...state.user[appUsername],
|
...state.user.sessionUser,
|
||||||
userToken: null
|
userToken: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,13 +412,12 @@ export const reducer = handleActions(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
[actionTypes.clearExamResults]: state => {
|
[actionTypes.clearExamResults]: state => {
|
||||||
const { appUsername } = state;
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[appUsername]: {
|
sessionUser: {
|
||||||
...state.user[appUsername],
|
...state.user.sessionUser,
|
||||||
examResults: null
|
examResults: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,14 +427,13 @@ export const reducer = handleActions(
|
|||||||
state,
|
state,
|
||||||
{ payload: { surveyResults } }
|
{ payload: { surveyResults } }
|
||||||
) => {
|
) => {
|
||||||
const { appUsername } = state;
|
const { completedSurveys = [] } = state.user.sessionUser;
|
||||||
const { completedSurveys = [] } = state.user[appUsername];
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[appUsername]: {
|
sessionUser: {
|
||||||
...state.user[appUsername],
|
...state.user.sessionUser,
|
||||||
completedSurveys: [...completedSurveys, surveyResults]
|
completedSurveys: [...completedSurveys, surveyResults]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,13 +444,12 @@ export const reducer = handleActions(
|
|||||||
currentChallengeId: payload
|
currentChallengeId: payload
|
||||||
}),
|
}),
|
||||||
[actionTypes.saveChallengeComplete]: (state, { payload }) => {
|
[actionTypes.saveChallengeComplete]: (state, { payload }) => {
|
||||||
const { appUsername } = state;
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[appUsername]: {
|
sessionUser: {
|
||||||
...state.user[appUsername],
|
...state.user.sessionUser,
|
||||||
savedChallenges: payload
|
savedChallenges: payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -478,8 +461,8 @@ export const reducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[state.appUsername]: {
|
sessionUser: {
|
||||||
...state.user[state.appUsername],
|
...state.user.sessionUser,
|
||||||
username: payload
|
username: payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,8 +494,8 @@ export const reducer = handleActions(
|
|||||||
...state,
|
...state,
|
||||||
user: {
|
user: {
|
||||||
...state.user,
|
...state.user,
|
||||||
[state.appUsername]: {
|
sessionUser: {
|
||||||
...state.user[state.appUsername],
|
...state.user.sessionUser,
|
||||||
profileUI: { ...payload }
|
profileUI: { ...payload }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,6 +308,7 @@ export type User = {
|
|||||||
about: string;
|
about: string;
|
||||||
acceptedPrivacyTerms: boolean;
|
acceptedPrivacyTerms: boolean;
|
||||||
completedChallenges: CompletedChallenge[];
|
completedChallenges: CompletedChallenge[];
|
||||||
|
completedChallengeCount: number;
|
||||||
completedSurveys: SurveyResults[];
|
completedSurveys: SurveyResults[];
|
||||||
currentChallengeId: string;
|
currentChallengeId: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { Certification } from '../../../shared/config/certification-settings';
|
|
||||||
import superBlockStructure from '../../../curriculum/superblock-structure/full-stack.json';
|
import superBlockStructure from '../../../curriculum/superblock-structure/full-stack.json';
|
||||||
import { randomBetween } from '../utils/random-between';
|
import { randomBetween } from '../utils/random-between';
|
||||||
import { getSessionChallengeData } from '../utils/session-storage';
|
import { getSessionChallengeData } from '../utils/session-storage';
|
||||||
import { ns as MainApp } from './action-types';
|
import { ns as MainApp } from './action-types';
|
||||||
|
|
||||||
export const savedChallengesSelector = state =>
|
export const savedChallengesSelector = state =>
|
||||||
userSelector(state).savedChallenges || [];
|
userSelector(state)?.savedChallenges || [];
|
||||||
export const completedChallengesSelector = state =>
|
export const completedChallengesSelector = state =>
|
||||||
userSelector(state).completedChallenges || [];
|
userSelector(state)?.completedChallenges || [];
|
||||||
export const userIdSelector = state => userSelector(state).id;
|
export const userIdSelector = state => userSelector(state)?.id;
|
||||||
export const partiallyCompletedChallengesSelector = state =>
|
export const partiallyCompletedChallengesSelector = state =>
|
||||||
userSelector(state).partiallyCompletedChallenges || [];
|
userSelector(state)?.partiallyCompletedChallenges || [];
|
||||||
export const currentChallengeIdSelector = state =>
|
export const currentChallengeIdSelector = state =>
|
||||||
state[MainApp].currentChallengeId;
|
state[MainApp].currentChallengeId;
|
||||||
export const isRandomCompletionThresholdSelector = state =>
|
export const isRandomCompletionThresholdSelector = state =>
|
||||||
state[MainApp].isRandomCompletionThreshold;
|
state[MainApp].isRandomCompletionThreshold;
|
||||||
export const isDonatingSelector = state => userSelector(state).isDonating;
|
export const isDonatingSelector = state => userSelector(state)?.isDonating;
|
||||||
export const isOnlineSelector = state => state[MainApp].isOnline;
|
export const isOnlineSelector = state => state[MainApp].isOnline;
|
||||||
export const isServerOnlineSelector = state => state[MainApp].isServerOnline;
|
export const isServerOnlineSelector = state => state[MainApp].isServerOnline;
|
||||||
export const isSignedInSelector = state => !!state[MainApp].appUsername;
|
export const isSignedInSelector = state => !!userSelector(state);
|
||||||
export const isDonationModalOpenSelector = state =>
|
export const isDonationModalOpenSelector = state =>
|
||||||
state[MainApp].showDonationModal;
|
state[MainApp].showDonationModal;
|
||||||
export const isSignoutModalOpenSelector = state =>
|
export const isSignoutModalOpenSelector = state =>
|
||||||
@@ -88,185 +87,28 @@ export const shouldRequestDonationSelector = state => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userTokenSelector = state => {
|
export const userTokenSelector = state => userSelector(state)?.userToken;
|
||||||
return userSelector(state).userToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const examInProgressSelector = state => {
|
export const examInProgressSelector = state => state[MainApp].examInProgress;
|
||||||
return state[MainApp].examInProgress;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const examResultsSelector = state => userSelector(state).examResults;
|
export const examResultsSelector = state => userSelector(state)?.examResults;
|
||||||
|
|
||||||
export const msUsernameSelector = state => {
|
export const msUsernameSelector = state => userSelector(state)?.msUsername;
|
||||||
return userSelector(state).msUsername;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const completedSurveysSelector = state =>
|
export const completedSurveysSelector = state =>
|
||||||
userSelector(state).completedSurveys || [];
|
userSelector(state)?.completedSurveys || [];
|
||||||
|
|
||||||
export const isProcessingSelector = state => {
|
export const isProcessingSelector = state => {
|
||||||
return state[MainApp].isProcessing;
|
return state[MainApp].isProcessing;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userByNameSelector = username => state => {
|
export const createUserByNameSelector = username => state => {
|
||||||
const { user } = state[MainApp];
|
const sessionUser = userSelector(state);
|
||||||
// return initial state empty user empty object instead of empty
|
const otherUser = otherUserSelector(state);
|
||||||
// object literal to prevent components from re-rendering unnecessarily
|
const isSessionUser = sessionUser?.username === username;
|
||||||
// TODO: confirm if "initialState" can be moved here or action-types.js
|
const isOtherUser = otherUser?.username === username;
|
||||||
return user[username] ?? {};
|
const user = isSessionUser ? sessionUser : isOtherUser ? otherUser : null;
|
||||||
};
|
return 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,
|
|
||||||
isCollegeAlgebraPyCertV8,
|
|
||||||
isFoundationalCSharpCertV8,
|
|
||||||
isJsAlgoDataStructCertV8
|
|
||||||
} = userByNameSelector(username)(state);
|
|
||||||
return {
|
|
||||||
hasModernCert:
|
|
||||||
isRespWebDesignCert ||
|
|
||||||
is2018DataVisCert ||
|
|
||||||
isFrontEndLibsCert ||
|
|
||||||
isApisMicroservicesCert ||
|
|
||||||
isQaCertV7 ||
|
|
||||||
isInfosecCertV7 ||
|
|
||||||
isFullStackCert ||
|
|
||||||
isSciCompPyCertV7 ||
|
|
||||||
isDataAnalysisPyCertV7 ||
|
|
||||||
isMachineLearningPyCertV7 ||
|
|
||||||
isRelationalDatabaseCertV8 ||
|
|
||||||
isCollegeAlgebraPyCertV8 ||
|
|
||||||
isFoundationalCSharpCertV8 ||
|
|
||||||
isJsAlgoDataStructCertV8,
|
|
||||||
hasLegacyCert:
|
|
||||||
isFrontEndCert ||
|
|
||||||
isJsAlgoDataStructCert ||
|
|
||||||
isBackEndCert ||
|
|
||||||
isDataVisCert ||
|
|
||||||
isInfosecQaCert,
|
|
||||||
isFullStackCert,
|
|
||||||
currentCerts: [
|
|
||||||
{
|
|
||||||
show: isRespWebDesignCert,
|
|
||||||
title: 'Responsive Web Design Certification',
|
|
||||||
certSlug: Certification.RespWebDesign
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isJsAlgoDataStructCertV8,
|
|
||||||
title: 'JavaScript Algorithms and Data Structures Certification',
|
|
||||||
certSlug: Certification.JsAlgoDataStructNew
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isFrontEndLibsCert,
|
|
||||||
title: 'Front End Development Libraries Certification',
|
|
||||||
certSlug: Certification.FrontEndDevLibs
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: is2018DataVisCert,
|
|
||||||
title: 'Data Visualization Certification',
|
|
||||||
certSlug: Certification.DataVis
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isRelationalDatabaseCertV8,
|
|
||||||
title: 'Relational Database Certification',
|
|
||||||
certSlug: Certification.RelationalDb
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isApisMicroservicesCert,
|
|
||||||
title: 'Back End Development and APIs Certification',
|
|
||||||
certSlug: Certification.BackEndDevApis
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isQaCertV7,
|
|
||||||
title: 'Quality Assurance Certification',
|
|
||||||
certSlug: Certification.QualityAssurance
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isSciCompPyCertV7,
|
|
||||||
title: 'Scientific Computing with Python Certification',
|
|
||||||
certSlug: Certification.SciCompPy
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isDataAnalysisPyCertV7,
|
|
||||||
title: 'Data Analysis with Python Certification',
|
|
||||||
certSlug: Certification.DataAnalysisPy
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isInfosecCertV7,
|
|
||||||
title: 'Information Security Certification',
|
|
||||||
certSlug: Certification.InfoSec
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isMachineLearningPyCertV7,
|
|
||||||
title: 'Machine Learning with Python Certification',
|
|
||||||
certSlug: Certification.MachineLearningPy
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isCollegeAlgebraPyCertV8,
|
|
||||||
title: 'College Algebra with Python Certification',
|
|
||||||
certSlug: Certification.CollegeAlgebraPy
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isFoundationalCSharpCertV8,
|
|
||||||
title: 'Foundational C# with Microsoft Certification',
|
|
||||||
certSlug: Certification.FoundationalCSharp
|
|
||||||
}
|
|
||||||
],
|
|
||||||
legacyCerts: [
|
|
||||||
{
|
|
||||||
show: isFrontEndCert,
|
|
||||||
title: 'Front End Certification',
|
|
||||||
certSlug: Certification.LegacyFrontEnd
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isJsAlgoDataStructCert,
|
|
||||||
title: 'Legacy JavaScript Algorithms and Data Structures Certification',
|
|
||||||
certSlug: Certification.JsAlgoDataStruct
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isBackEndCert,
|
|
||||||
title: 'Back End Certification',
|
|
||||||
certSlug: Certification.LegacyBackEnd
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isDataVisCert,
|
|
||||||
title: 'Data Visualization Certification',
|
|
||||||
certSlug: Certification.LegacyDataVis
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isInfosecQaCert,
|
|
||||||
title: 'Information Security and Quality Assurance Certification',
|
|
||||||
// Keep the current public profile cert slug
|
|
||||||
certSlug: Certification.LegacyInfoSecQa
|
|
||||||
},
|
|
||||||
{
|
|
||||||
show: isFullStackCert,
|
|
||||||
title: 'Full Stack Certification',
|
|
||||||
// Keep the current public profile cert slug
|
|
||||||
certSlug: Certification.LegacyFullStack
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userFetchStateSelector = state => state[MainApp].userFetchState;
|
export const userFetchStateSelector = state => state[MainApp].userFetchState;
|
||||||
@@ -343,15 +185,11 @@ export const completionStateSelector = createSelector(
|
|||||||
);
|
);
|
||||||
export const userProfileFetchStateSelector = state =>
|
export const userProfileFetchStateSelector = state =>
|
||||||
state[MainApp].userProfileFetchState;
|
state[MainApp].userProfileFetchState;
|
||||||
export const usernameSelector = state => state[MainApp].appUsername;
|
export const usernameSelector = state => userSelector(state)?.username ?? '';
|
||||||
export const themeSelector = state => state[MainApp].theme;
|
export const themeSelector = state => state[MainApp].theme;
|
||||||
export const userThemeSelector = state => {
|
export const userThemeSelector = state => userSelector(state)?.theme;
|
||||||
return userSelector(state).theme;
|
|
||||||
};
|
|
||||||
export const userSelector = state => {
|
|
||||||
const username = usernameSelector(state);
|
|
||||||
|
|
||||||
return state[MainApp].user[username] || {};
|
export const userSelector = state => state[MainApp].user.sessionUser;
|
||||||
};
|
export const otherUserSelector = state => state[MainApp].user.otherUser;
|
||||||
|
|
||||||
export const renderStartTimeSelector = state => state[MainApp].renderStartTime;
|
export const renderStartTimeSelector = state => state[MainApp].renderStartTime;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
takeLatest
|
takeLatest
|
||||||
} from 'redux-saga/effects';
|
} from 'redux-saga/effects';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
import { navigate } from 'gatsby';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
certTypeIdMap,
|
certTypeIdMap,
|
||||||
@@ -69,6 +70,8 @@ function* submitNewUsernameSaga({ payload: username }) {
|
|||||||
try {
|
try {
|
||||||
const { data } = yield call(putUpdateMyUsername, username);
|
const { data } = yield call(putUpdateMyUsername, username);
|
||||||
yield put(submitNewUsernameComplete({ ...data, username }));
|
yield put(submitNewUsernameComplete({ ...data, username }));
|
||||||
|
// When the username is updated, the user would otherwise still be on their old profile:
|
||||||
|
navigate(`/${username}`);
|
||||||
yield put(createFlashMessage(data));
|
yield put(createFlashMessage(data));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
yield put(submitNewUsernameError(e));
|
yield put(submitNewUsernameError(e));
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export type FlashMessageArg = {
|
|||||||
export interface State {
|
export interface State {
|
||||||
[FlashApp]: FlashState;
|
[FlashApp]: FlashState;
|
||||||
[MainApp]: {
|
[MainApp]: {
|
||||||
appUsername: string;
|
|
||||||
recentlyClaimedBlock: null | string;
|
recentlyClaimedBlock: null | string;
|
||||||
showMultipleProgressModals: boolean;
|
showMultipleProgressModals: boolean;
|
||||||
currentChallengId: string;
|
currentChallengId: string;
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { createSelector } from 'reselect';
|
|||||||
import type {
|
import type {
|
||||||
ChallengeFiles,
|
ChallengeFiles,
|
||||||
Test,
|
Test,
|
||||||
User,
|
ChallengeMeta,
|
||||||
ChallengeMeta
|
User
|
||||||
} from '../../../redux/prop-types';
|
} from '../../../redux/prop-types';
|
||||||
import { userSelector } from '../../../redux/selectors';
|
import { userSelector } from '../../../redux/selectors';
|
||||||
import {
|
import {
|
||||||
@@ -49,7 +49,7 @@ const mapStateToProps = createSelector(
|
|||||||
canFocusEditor: boolean,
|
canFocusEditor: boolean,
|
||||||
challengeFiles: ChallengeFiles,
|
challengeFiles: ChallengeFiles,
|
||||||
tests: Test[],
|
tests: Test[],
|
||||||
user: User,
|
user: User | null,
|
||||||
{ nextChallengePath, prevChallengePath }: ChallengeMeta
|
{ nextChallengePath, prevChallengePath }: ChallengeMeta
|
||||||
) => ({
|
) => ({
|
||||||
isHelpModalOpen,
|
isHelpModalOpen,
|
||||||
@@ -59,7 +59,7 @@ const mapStateToProps = createSelector(
|
|||||||
canFocusEditor,
|
canFocusEditor,
|
||||||
challengeFiles,
|
challengeFiles,
|
||||||
tests,
|
tests,
|
||||||
user,
|
keyboardShortcuts: !!user?.keyboardShortcuts,
|
||||||
nextChallengePath,
|
nextChallengePath,
|
||||||
prevChallengePath
|
prevChallengePath
|
||||||
})
|
})
|
||||||
@@ -101,7 +101,7 @@ export type HotkeysProps = Pick<
|
|||||||
setIsAdvancing: (arg0: boolean) => void;
|
setIsAdvancing: (arg0: boolean) => void;
|
||||||
openShortcutsModal: () => void;
|
openShortcutsModal: () => void;
|
||||||
playScene?: () => void;
|
playScene?: () => void;
|
||||||
user: User;
|
keyboardShortcuts: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function Hotkeys({
|
function Hotkeys({
|
||||||
@@ -121,7 +121,7 @@ function Hotkeys({
|
|||||||
usesMultifileEditor,
|
usesMultifileEditor,
|
||||||
openShortcutsModal,
|
openShortcutsModal,
|
||||||
playScene,
|
playScene,
|
||||||
user: { keyboardShortcuts },
|
keyboardShortcuts,
|
||||||
isHelpModalOpen,
|
isHelpModalOpen,
|
||||||
isResetModalOpen,
|
isResetModalOpen,
|
||||||
isShortcutsModalOpen,
|
isShortcutsModalOpen,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { closeModal } from '../redux/actions';
|
|||||||
import { isShortcutsModalOpenSelector } from '../redux/selectors';
|
import { isShortcutsModalOpenSelector } from '../redux/selectors';
|
||||||
import { updateMyKeyboardShortcuts } from '../../../redux/settings/actions';
|
import { updateMyKeyboardShortcuts } from '../../../redux/settings/actions';
|
||||||
import { userSelector } from '../../../redux/selectors';
|
import { userSelector } from '../../../redux/selectors';
|
||||||
import { User } from '../../../redux/prop-types';
|
import type { User } from '../../../redux/prop-types';
|
||||||
import KeyboardShortcutsSettings from '../../../components/settings/keyboard-shortcuts';
|
import KeyboardShortcutsSettings from '../../../components/settings/keyboard-shortcuts';
|
||||||
|
|
||||||
import './shortcuts-modal.css';
|
import './shortcuts-modal.css';
|
||||||
@@ -19,13 +19,16 @@ interface ShortcutsModalProps {
|
|||||||
toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void;
|
toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
t: (text: string) => string;
|
t: (text: string) => string;
|
||||||
user: User;
|
keyboardShortcuts: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
isShortcutsModalOpenSelector,
|
isShortcutsModalOpenSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
(isOpen: boolean, user: User) => ({ isOpen, user })
|
(isOpen: boolean, user: User | null) => ({
|
||||||
|
isOpen,
|
||||||
|
keyboardShortcuts: !!user?.keyboardShortcuts
|
||||||
|
})
|
||||||
);
|
);
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
@@ -42,7 +45,7 @@ function ShortcutsModal({
|
|||||||
toggleKeyboardShortcuts,
|
toggleKeyboardShortcuts,
|
||||||
isOpen,
|
isOpen,
|
||||||
t,
|
t,
|
||||||
user: { keyboardShortcuts }
|
keyboardShortcuts
|
||||||
}: ShortcutsModalProps): JSX.Element {
|
}: ShortcutsModalProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Modal onClose={closeShortcutsModal} open={isOpen}>
|
<Modal onClose={closeShortcutsModal} open={isOpen}>
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ import { SuperBlocks } from '../../../../../shared/config/curriculum';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
userFetchStateSelector,
|
userFetchStateSelector
|
||||||
currentCertsSelector
|
|
||||||
} from '../../../redux/selectors';
|
} from '../../../redux/selectors';
|
||||||
import { User, Steps } from '../../../redux/prop-types';
|
import { User } from '../../../redux/prop-types';
|
||||||
import {
|
import {
|
||||||
type CertTitle,
|
type CertTitle,
|
||||||
liveCerts
|
liveCerts
|
||||||
} from '../../../../config/cert-and-project-map';
|
} from '../../../../config/cert-and-project-map';
|
||||||
|
import { getCertifications } from '../../../components/profile/components/utils/certification';
|
||||||
|
|
||||||
interface CertChallengeProps {
|
interface CertChallengeProps {
|
||||||
// TODO: create enum/reuse SuperBlocks enum somehow
|
// TODO: create enum/reuse SuperBlocks enum somehow
|
||||||
@@ -31,7 +31,6 @@ interface CertChallengeProps {
|
|||||||
error: null | string;
|
error: null | string;
|
||||||
};
|
};
|
||||||
isSignedIn: boolean;
|
isSignedIn: boolean;
|
||||||
currentCerts: Steps['currentCerts'];
|
|
||||||
superBlock: SuperBlocks;
|
superBlock: SuperBlocks;
|
||||||
title: CertTitle;
|
title: CertTitle;
|
||||||
user: User;
|
user: User;
|
||||||
@@ -39,15 +38,9 @@ interface CertChallengeProps {
|
|||||||
|
|
||||||
const mapStateToProps = (state: unknown) => {
|
const mapStateToProps = (state: unknown) => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
currentCertsSelector,
|
|
||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
(
|
(fetchState: CertChallengeProps['fetchState'], isSignedIn) => ({
|
||||||
currentCerts,
|
|
||||||
fetchState: CertChallengeProps['fetchState'],
|
|
||||||
isSignedIn
|
|
||||||
) => ({
|
|
||||||
currentCerts,
|
|
||||||
fetchState,
|
fetchState,
|
||||||
isSignedIn
|
isSignedIn
|
||||||
})
|
})
|
||||||
@@ -55,17 +48,19 @@ const mapStateToProps = (state: unknown) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CertChallenge = ({
|
const CertChallenge = ({
|
||||||
currentCerts,
|
|
||||||
superBlock,
|
superBlock,
|
||||||
title,
|
title,
|
||||||
fetchState,
|
fetchState,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
user: { username }
|
user
|
||||||
}: CertChallengeProps): JSX.Element => {
|
}: CertChallengeProps): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isCertified, setIsCertified] = useState(false);
|
const [isCertified, setIsCertified] = useState(false);
|
||||||
const [userLoaded, setUserLoaded] = useState(false);
|
const [userLoaded, setUserLoaded] = useState(false);
|
||||||
|
|
||||||
|
const { currentCerts } = getCertifications(user);
|
||||||
|
const { username } = user;
|
||||||
|
|
||||||
const cert = liveCerts.find(x => x.title === title);
|
const cert = liveCerts.find(x => x.title === title);
|
||||||
if (!cert) throw Error(`Certification ${title} not found`);
|
if (!cert) throw Error(`Certification ${title} not found`);
|
||||||
const certSlug = cert.certSlug;
|
const certSlug = cert.certSlug;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ type SuperBlockProps = {
|
|||||||
resetExpansion: () => void;
|
resetExpansion: () => void;
|
||||||
toggleBlock: (arg0: string) => void;
|
toggleBlock: (arg0: string) => void;
|
||||||
tryToShowDonationModal: () => void;
|
tryToShowDonationModal: () => void;
|
||||||
user: User;
|
user: User | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
configureAnchors({ offset: -40, scrollDuration: 0 });
|
configureAnchors({ offset: -40, scrollDuration: 0 });
|
||||||
@@ -101,7 +101,7 @@ const mapStateToProps = (state: Record<string, unknown>) => {
|
|||||||
isSignedIn,
|
isSignedIn,
|
||||||
signInLoading: boolean,
|
signInLoading: boolean,
|
||||||
fetchState: FetchState,
|
fetchState: FetchState,
|
||||||
user: User
|
user: User | null
|
||||||
) => ({
|
) => ({
|
||||||
currentChallengeId,
|
currentChallengeId,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
@@ -147,8 +147,7 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
|
|||||||
signInLoading,
|
signInLoading,
|
||||||
user,
|
user,
|
||||||
pageContext: { superBlock, title, certification },
|
pageContext: { superBlock, title, certification },
|
||||||
location,
|
location
|
||||||
user: { completedChallenges: allCompletedChallenges }
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const allChallenges = useMemo(
|
const allChallenges = useMemo(
|
||||||
@@ -163,10 +162,10 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
|
|||||||
|
|
||||||
const completedChallenges = useMemo(
|
const completedChallenges = useMemo(
|
||||||
() =>
|
() =>
|
||||||
allCompletedChallenges.filter(completedChallenge =>
|
(user?.completedChallenges ?? []).filter(completedChallenge =>
|
||||||
superBlockChallenges.some(c => c.id === completedChallenge.id)
|
superBlockChallenges.some(c => c.id === completedChallenge.id)
|
||||||
),
|
),
|
||||||
[superBlockChallenges, allCompletedChallenges]
|
[superBlockChallenges, user?.completedChallenges]
|
||||||
);
|
);
|
||||||
|
|
||||||
const i18nTitle = i18next.t(`intro:${superBlock}.title`);
|
const i18nTitle = i18next.t(`intro:${superBlock}.title`);
|
||||||
@@ -251,7 +250,7 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
|
|||||||
onCertificationDonationAlertClick={
|
onCertificationDonationAlertClick={
|
||||||
onCertificationDonationAlertClick
|
onCertificationDonationAlertClick
|
||||||
}
|
}
|
||||||
isDonating={user.isDonating}
|
isDonating={user?.isDonating ?? false}
|
||||||
/>
|
/>
|
||||||
<HelpTranslate superBlock={superBlock} />
|
<HelpTranslate superBlock={superBlock} />
|
||||||
<Spacer size='l' />
|
<Spacer size='l' />
|
||||||
@@ -284,7 +283,7 @@ const SuperBlockIntroductionPage = (props: SuperBlockProps) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{showCertification && (
|
{showCertification && !!user && (
|
||||||
<CertChallenge
|
<CertChallenge
|
||||||
certification={certification}
|
certification={certification}
|
||||||
superBlock={superBlock}
|
superBlock={superBlock}
|
||||||
|
|||||||
@@ -93,10 +93,6 @@ async function request<T>(
|
|||||||
|
|
||||||
/** GET **/
|
/** GET **/
|
||||||
|
|
||||||
interface SessionUser {
|
|
||||||
user?: { [username: string]: User };
|
|
||||||
}
|
|
||||||
|
|
||||||
type CompleteChallengeFromApi = {
|
type CompleteChallengeFromApi = {
|
||||||
files: Array<Omit<ChallengeFile, 'fileKey'> & { key: string }>;
|
files: Array<Omit<ChallengeFile, 'fileKey'> & { key: string }>;
|
||||||
} & Omit<CompletedChallenge, 'challengeFiles'>;
|
} & Omit<CompletedChallenge, 'challengeFiles'>;
|
||||||
@@ -105,26 +101,19 @@ type SavedChallengeFromApi = {
|
|||||||
files: Array<Omit<SavedChallengeFile, 'fileKey'> & { key: string }>;
|
files: Array<Omit<SavedChallengeFile, 'fileKey'> & { key: string }>;
|
||||||
} & Omit<SavedChallenge, 'challengeFiles'>;
|
} & Omit<SavedChallenge, 'challengeFiles'>;
|
||||||
|
|
||||||
type ApiSessionResponse = Omit<SessionUser, 'user'>;
|
type ApiUser = Omit<User, 'completedChallenges' & 'savedChallenges'> & {
|
||||||
type ApiUser = {
|
completedChallenges?: CompleteChallengeFromApi[];
|
||||||
|
savedChallenges?: SavedChallengeFromApi[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiUserResponse = {
|
||||||
user: {
|
user: {
|
||||||
[username: string]: Omit<
|
[username: string]: ApiUser;
|
||||||
User,
|
|
||||||
'completedChallenges' & 'savedChallenges'
|
|
||||||
> & {
|
|
||||||
completedChallenges?: CompleteChallengeFromApi[];
|
|
||||||
savedChallenges?: SavedChallengeFromApi[];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
result?: string;
|
result?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UserResponse = {
|
function parseApiResponseToClientUser(data: ApiUserResponse): User | null {
|
||||||
user: { [username: string]: User } | Record<string, never>;
|
|
||||||
result: string | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseApiResponseToClientUser(data: ApiUser): UserResponse {
|
|
||||||
const userData = data.user?.[data?.result ?? ''];
|
const userData = data.user?.[data?.result ?? ''];
|
||||||
let completedChallenges: CompletedChallenge[] = [];
|
let completedChallenges: CompletedChallenge[] = [];
|
||||||
let savedChallenges: SavedChallenge[] = [];
|
let savedChallenges: SavedChallenge[] = [];
|
||||||
@@ -134,12 +123,9 @@ function parseApiResponseToClientUser(data: ApiUser): UserResponse {
|
|||||||
);
|
);
|
||||||
savedChallenges = mapFilesToChallengeFiles(userData.savedChallenges);
|
savedChallenges = mapFilesToChallengeFiles(userData.savedChallenges);
|
||||||
}
|
}
|
||||||
return {
|
return data.result
|
||||||
user: {
|
? { ...userData, completedChallenges, savedChallenges }
|
||||||
[data.result ?? '']: { ...userData, completedChallenges, savedChallenges }
|
: null;
|
||||||
},
|
|
||||||
result: data.result
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this at least needs a few aliases so it's human readable
|
// TODO: this at least needs a few aliases so it's human readable
|
||||||
@@ -158,44 +144,38 @@ function mapKeyToFileKey<K>(
|
|||||||
return files.map(({ key, ...rest }) => ({ ...rest, fileKey: key }));
|
return files.map(({ key, ...rest }) => ({ ...rest, fileKey: key }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSessionUser(): Promise<ResponseWithData<SessionUser>> {
|
export function getSessionUser(): Promise<ResponseWithData<User | null>> {
|
||||||
const responseWithData: Promise<
|
const responseWithData: Promise<ResponseWithData<ApiUserResponse>> = get(
|
||||||
ResponseWithData<ApiUser & ApiSessionResponse>
|
'/user/get-session-user'
|
||||||
> = get('/user/get-session-user');
|
);
|
||||||
// TODO: Once DB is migrated, no longer need to parse `files` -> `challengeFiles` etc.
|
// TODO: Once DB is migrated, no longer need to parse `files` -> `challengeFiles` etc.
|
||||||
return responseWithData.then(({ response, data }) => {
|
return responseWithData.then(({ response, data }) => {
|
||||||
const { result, user } = parseApiResponseToClientUser(data);
|
const user = parseApiResponseToClientUser(data);
|
||||||
return {
|
return {
|
||||||
response,
|
response,
|
||||||
data: {
|
data: user
|
||||||
result,
|
|
||||||
user
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserProfileResponse = {
|
type UserProfileResponse = {
|
||||||
entities: Omit<UserResponse, 'result'>;
|
entities: Omit<ApiUserResponse, 'result'>;
|
||||||
result: string | undefined;
|
result: string | undefined;
|
||||||
};
|
};
|
||||||
export function getUserProfile(
|
export function getUserProfile(
|
||||||
username: string
|
username: string
|
||||||
): Promise<ResponseWithData<UserProfileResponse>> {
|
): Promise<ResponseWithData<User | null>> {
|
||||||
const responseWithData = get<{ entities?: ApiUser; result?: string }>(
|
const responseWithData = get<UserProfileResponse>(
|
||||||
`/users/get-public-profile?username=${username}`
|
`/users/get-public-profile?username=${username}`
|
||||||
);
|
);
|
||||||
return responseWithData.then(({ response, data }) => {
|
return responseWithData.then(({ response, data }) => {
|
||||||
const { result, user } = parseApiResponseToClientUser({
|
const user = parseApiResponseToClientUser({
|
||||||
user: data.entities?.user ?? {},
|
user: data.entities?.user ?? {},
|
||||||
result: data.result
|
result: data.result
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
response,
|
response,
|
||||||
data: {
|
data: user
|
||||||
entities: { user },
|
|
||||||
result
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ test.describe('Username Settings Validation', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByRole('alert').filter({ hasText: flashText }).first()
|
page.getByRole('alert').filter({ hasText: flashText }).first()
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
await expect(page).toHaveURL(`/${settingsObject.usernameAvailable}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update username in lowercase and reflect in the UI', async ({
|
test('should update username in lowercase and reflect in the UI', async ({
|
||||||
|
|||||||
Reference in New Issue
Block a user