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