chore: remove verify-can-claim-cert logic (#44574)
* chore: remove verify-can-claim-cert logic * remove extraneous * remove console log before Nich wakes up * add api route back with flash * remove unnecessary logic in completion-epic * change tests for new layout * dynamically use api location * rename file * fix Cypress api location * fix(test): anchor does not have disabled class * fix(tests): change js test to claim from /settings * chore: change status to 410 (gone) * update testing again * oliver is nitpicky Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * make oliver happy Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -63,7 +63,8 @@
|
||||
"submit-and-go": "Submit and go to next challenge",
|
||||
"go-to-next": "Go to next challenge",
|
||||
"ask-later": "Ask me later",
|
||||
"start-coding": "Start coding!"
|
||||
"start-coding": "Start coding!",
|
||||
"go-to-settings": "Go to settings to claim your certification"
|
||||
},
|
||||
"landing": {
|
||||
"big-heading-1": "Learn to code — for free.",
|
||||
|
@@ -189,19 +189,7 @@ export const completedChallengesSelector = state =>
|
||||
export const completionCountSelector = state => state[MainApp].completionCount;
|
||||
export const currentChallengeIdSelector = state =>
|
||||
state[MainApp].currentChallengeId;
|
||||
export const stepsToClaimSelector = state => {
|
||||
const user = userSelector(state);
|
||||
const currentCerts = certificatesByNameSelector(user.username)(
|
||||
state
|
||||
).currentCerts;
|
||||
return {
|
||||
currentCerts: currentCerts,
|
||||
isHonest: user?.isHonest,
|
||||
isShowName: user?.profileUI?.showName,
|
||||
isShowCerts: user?.profileUI?.showCerts,
|
||||
isShowProfile: !user?.profileUI?.isLocked
|
||||
};
|
||||
};
|
||||
|
||||
export const emailSelector = state => userSelector(state).email;
|
||||
export const isAVariantSelector = state => {
|
||||
const email = emailSelector(state);
|
||||
@@ -271,6 +259,9 @@ export const userByNameSelector = username => state => {
|
||||
return user[username] ?? initialState.user;
|
||||
};
|
||||
|
||||
export const currentCertsSelector = state =>
|
||||
certificatesByNameSelector(state[MainApp]?.appUsername)(state)?.currentCerts;
|
||||
|
||||
export const certificatesByNameSelector = username => state => {
|
||||
const {
|
||||
isRespWebDesignCert,
|
||||
|
@@ -17,11 +17,9 @@ import {
|
||||
isSignedInSelector,
|
||||
submitComplete,
|
||||
updateComplete,
|
||||
updateFailed,
|
||||
usernameSelector
|
||||
updateFailed
|
||||
} from '../../../redux';
|
||||
|
||||
import { getVerifyCanClaimCert } from '../../../utils/ajax';
|
||||
import postUpdate$ from '../utils/postUpdate$';
|
||||
import { actionTypes } from './action-types';
|
||||
import {
|
||||
@@ -151,8 +149,7 @@ export default function completionEpic(action$, state$) {
|
||||
switchMap(({ type }) => {
|
||||
const state = state$.value;
|
||||
const meta = challengeMetaSelector(state);
|
||||
const { nextChallengePath, challengeType, superBlock, certification } =
|
||||
meta;
|
||||
const { nextChallengePath, challengeType, superBlock } = meta;
|
||||
const closeChallengeModal = of(closeModal('completion'));
|
||||
|
||||
let submitter = () => of({ type: 'no-user-signed-in' });
|
||||
@@ -170,13 +167,7 @@ export default function completionEpic(action$, state$) {
|
||||
}
|
||||
|
||||
const pathToNavigateTo = async () => {
|
||||
return await findPathToNavigateTo(
|
||||
certification,
|
||||
nextChallengePath,
|
||||
superBlock,
|
||||
state,
|
||||
challengeType
|
||||
);
|
||||
return await findPathToNavigateTo(nextChallengePath, superBlock);
|
||||
};
|
||||
|
||||
return submitter(type, state).pipe(
|
||||
@@ -188,38 +179,10 @@ export default function completionEpic(action$, state$) {
|
||||
);
|
||||
}
|
||||
|
||||
async function findPathToNavigateTo(
|
||||
certification,
|
||||
nextChallengePath,
|
||||
superBlock,
|
||||
state,
|
||||
challengeType
|
||||
) {
|
||||
let canClaimCert = false;
|
||||
const isProjectSubmission = [
|
||||
challengeTypes.frontEndProject,
|
||||
challengeTypes.backEndProject,
|
||||
challengeTypes.pythonProject
|
||||
].includes(challengeType);
|
||||
if (isProjectSubmission) {
|
||||
const username = usernameSelector(state);
|
||||
try {
|
||||
const response = await getVerifyCanClaimCert(username, certification);
|
||||
if (response.status === 200) {
|
||||
canClaimCert = response.data?.response?.message === 'can-claim-cert';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('failed to verify if user can claim certificate', err);
|
||||
}
|
||||
}
|
||||
let pathToNavigateTo;
|
||||
|
||||
if (nextChallengePath.includes(superBlock) && !canClaimCert) {
|
||||
pathToNavigateTo = nextChallengePath;
|
||||
} else if (canClaimCert) {
|
||||
pathToNavigateTo = `/learn/${superBlock}/#claim-cert-block`;
|
||||
async function findPathToNavigateTo(nextChallengePath, superBlock) {
|
||||
if (nextChallengePath.includes(superBlock)) {
|
||||
return nextChallengePath;
|
||||
} else {
|
||||
pathToNavigateTo = `/learn/${superBlock}/#${superBlock}-projects`;
|
||||
return `/learn/${superBlock}/#${superBlock}-projects`;
|
||||
}
|
||||
return pathToNavigateTo;
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Button } from '@freecodecamp/react-bootstrap';
|
||||
import { navigate } from 'gatsby-link';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, MouseEvent } from 'react';
|
||||
import { TFunction, withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
@@ -13,14 +13,12 @@ import { createFlashMessage } from '../../../components/Flash/redux';
|
||||
import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
|
||||
import {
|
||||
userFetchStateSelector,
|
||||
stepsToClaimSelector,
|
||||
isSignedInSelector
|
||||
isSignedInSelector,
|
||||
currentCertsSelector
|
||||
} from '../../../redux';
|
||||
import { User, Steps } from '../../../redux/prop-types';
|
||||
import { verifyCert } from '../../../redux/settings';
|
||||
import { certMap } from '../../../resources/cert-and-project-map';
|
||||
import { getVerifyCanClaimCert } from '../../../utils/ajax';
|
||||
import CertificationCard from './certification-card';
|
||||
|
||||
interface CertChallengeProps {
|
||||
// TODO: create enum/reuse SuperBlocks enum somehow
|
||||
@@ -33,7 +31,7 @@ interface CertChallengeProps {
|
||||
error: null | string;
|
||||
};
|
||||
isSignedIn: boolean;
|
||||
steps: Steps;
|
||||
currentCerts: Steps['currentCerts'];
|
||||
superBlock: SuperBlocks;
|
||||
t: TFunction;
|
||||
title: typeof certMap[number]['title'];
|
||||
@@ -48,11 +46,16 @@ const honestyInfoMessage = {
|
||||
|
||||
const mapStateToProps = (state: unknown) => {
|
||||
return createSelector(
|
||||
stepsToClaimSelector,
|
||||
currentCertsSelector,
|
||||
userFetchStateSelector,
|
||||
isSignedInSelector,
|
||||
(steps, fetchState: CertChallengeProps['fetchState'], isSignedIn) => ({
|
||||
steps,
|
||||
(
|
||||
currentCerts,
|
||||
fetchState: CertChallengeProps['fetchState'],
|
||||
isSignedIn
|
||||
) => ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
currentCerts,
|
||||
fetchState,
|
||||
isSignedIn
|
||||
})
|
||||
@@ -65,9 +68,8 @@ const mapDispatchToProps = {
|
||||
};
|
||||
|
||||
const CertChallenge = ({
|
||||
certification,
|
||||
createFlashMessage,
|
||||
steps = {},
|
||||
currentCerts,
|
||||
superBlock,
|
||||
t,
|
||||
verifyCert,
|
||||
@@ -76,45 +78,8 @@ const CertChallenge = ({
|
||||
isSignedIn,
|
||||
user: { isHonest, username }
|
||||
}: CertChallengeProps): JSX.Element => {
|
||||
const [canClaimCert, setCanClaimCert] = useState(false);
|
||||
const [certVerificationMessage, setCertVerificationMessage] = useState('');
|
||||
const [isCertified, setIsCertified] = useState(false);
|
||||
const [userLoaded, setUserLoaded] = useState(false);
|
||||
const [verificationComplete, setVerificationComplete] = useState(false);
|
||||
const [stepState, setStepState] = useState({
|
||||
numberOfSteps: 0,
|
||||
completedCount: 0
|
||||
});
|
||||
const [hasCompletedRequiredSteps, setHasCompletedRequiredSteps] =
|
||||
useState(false);
|
||||
const [isProjectsCompleted, setIsProjectsCompleted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (username) {
|
||||
void (async () => {
|
||||
try {
|
||||
const data = await getVerifyCanClaimCert(username, certification);
|
||||
if (data?.message) {
|
||||
setCanClaimCert(false);
|
||||
createFlashMessage(data.message);
|
||||
} else {
|
||||
const { status, result } = data?.response?.message;
|
||||
setCanClaimCert(status);
|
||||
setCertVerificationMessage(result);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
createFlashMessage({
|
||||
type: 'danger',
|
||||
message: FlashMessages.ReallyWeird
|
||||
});
|
||||
} finally {
|
||||
setVerificationComplete(true);
|
||||
}
|
||||
})();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [username]);
|
||||
|
||||
// @ts-expect-error Typescript is confused
|
||||
const certSlug = certMap.find(x => x.title === title).certSlug;
|
||||
@@ -133,40 +98,19 @@ const CertChallenge = ({
|
||||
|
||||
useEffect(() => {
|
||||
setIsCertified(
|
||||
steps?.currentCerts?.find(
|
||||
currentCerts?.find(
|
||||
(cert: { certSlug: string }) =>
|
||||
certSlugTypeMapTyped[cert.certSlug] ===
|
||||
superBlockCertTypeMapTyped[superBlock]
|
||||
)?.show ?? false
|
||||
);
|
||||
|
||||
const projectsCompleted =
|
||||
canClaimCert || certVerificationMessage === 'projects-completed';
|
||||
const projectsCompletedNumber = projectsCompleted ? 1 : 0;
|
||||
const completedCount =
|
||||
Object.values(steps).filter(
|
||||
stepVal => typeof stepVal === 'boolean' && stepVal
|
||||
).length + projectsCompletedNumber;
|
||||
const numberOfSteps = Object.keys(steps).length;
|
||||
setHasCompletedRequiredSteps(completedCount === numberOfSteps);
|
||||
setStepState({ numberOfSteps, completedCount });
|
||||
setIsProjectsCompleted(projectsCompleted);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [steps, canClaimCert, certVerificationMessage]);
|
||||
}, [currentCerts]);
|
||||
|
||||
const certLocation = `/certification/${username}/${certSlug}`;
|
||||
const i18nSuperBlock = t(`intro:${superBlock}.title`);
|
||||
const i18nCertText = t(`intro:misc-text.certification`, {
|
||||
cert: i18nSuperBlock
|
||||
});
|
||||
|
||||
const showCertificationCard =
|
||||
userLoaded &&
|
||||
isSignedIn &&
|
||||
(!isCertified || (!hasCompletedRequiredSteps && verificationComplete));
|
||||
|
||||
const createClickHandler =
|
||||
(certSlug: string | undefined) => (e: { preventDefault: () => void }) => {
|
||||
(certSlug: string | undefined) => (e: MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
if (isCertified) {
|
||||
return navigate(certLocation);
|
||||
@@ -177,33 +121,19 @@ const CertChallenge = ({
|
||||
};
|
||||
return (
|
||||
<div className='block'>
|
||||
{showCertificationCard && (
|
||||
<CertificationCard
|
||||
i18nCertText={i18nCertText}
|
||||
isProjectsCompleted={isProjectsCompleted}
|
||||
steps={steps}
|
||||
stepState={stepState}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
{isSignedIn && (
|
||||
<Button
|
||||
block={true}
|
||||
bsStyle='primary'
|
||||
className='cert-btn'
|
||||
href={isCertified ? certLocation : `/settings#certification-settings`}
|
||||
onClick={() => (isCertified ? createClickHandler(certSlug) : false)}
|
||||
>
|
||||
{isCertified && userLoaded
|
||||
? t('buttons.show-cert')
|
||||
: t('buttons.go-to-settings')}
|
||||
</Button>
|
||||
)}
|
||||
<>
|
||||
{isSignedIn && (
|
||||
<Button
|
||||
block={true}
|
||||
bsStyle='primary'
|
||||
className='cert-btn'
|
||||
disabled={
|
||||
!canClaimCert || (isCertified && !hasCompletedRequiredSteps)
|
||||
}
|
||||
href={certLocation}
|
||||
onClick={createClickHandler(certSlug)}
|
||||
>
|
||||
{isCertified && userLoaded
|
||||
? t('buttons.show-cert')
|
||||
: t('buttons.claim-cert')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -1,92 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ScrollableAnchor from 'react-scrollable-anchor';
|
||||
import { SuperBlocks } from '../../../../../config/certification-settings';
|
||||
import Caret from '../../../assets/icons/caret';
|
||||
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
|
||||
import GreenPass from '../../../assets/icons/green-pass';
|
||||
import { Steps } from '../../../redux/prop-types';
|
||||
import ClaimCertSteps from './claim-cert-steps';
|
||||
|
||||
interface CertificationCardProps {
|
||||
i18nCertText: string;
|
||||
isProjectsCompleted: boolean;
|
||||
stepState: {
|
||||
numberOfSteps: number;
|
||||
completedCount: number;
|
||||
};
|
||||
steps: Steps;
|
||||
superBlock: SuperBlocks;
|
||||
}
|
||||
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
|
||||
|
||||
const CertificationCard = ({
|
||||
isProjectsCompleted,
|
||||
superBlock,
|
||||
i18nCertText,
|
||||
stepState: { completedCount, numberOfSteps },
|
||||
steps
|
||||
}: CertificationCardProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
const handleBlockClick = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
const {
|
||||
expand: expandText,
|
||||
collapse: collapseText
|
||||
}: {
|
||||
expand: string;
|
||||
collapse: string;
|
||||
} = t('intro:misc-text');
|
||||
return (
|
||||
<ScrollableAnchor id='claim-cert-block'>
|
||||
<div className={`block ${isExpanded ? 'open' : ''}`}>
|
||||
<div className='block-title-wrapper'>
|
||||
<a className='block-link' href='#claim-cert-block'>
|
||||
<h3 className='big-block-title'>
|
||||
{t('certification-card.title')}
|
||||
<span className='block-link-icon'>#</span>
|
||||
</h3>
|
||||
</a>
|
||||
</div>
|
||||
<div className='block-description'>
|
||||
{t('certification-card.intro', { i18nCertText })}
|
||||
</div>
|
||||
<button
|
||||
aria-expanded={isExpanded}
|
||||
className='map-title'
|
||||
onClick={handleBlockClick}
|
||||
>
|
||||
<Caret />
|
||||
<h4 className='course-title'>
|
||||
{`${isExpanded ? collapseText : expandText}`}
|
||||
</h4>
|
||||
<div className='map-title-completed course-title'>
|
||||
{completedCount === numberOfSteps ? (
|
||||
<GreenPass style={mapIconStyle} />
|
||||
) : (
|
||||
<GreenNotCompleted style={mapIconStyle} />
|
||||
)}
|
||||
<span className='map-completed-count'>{`${completedCount}/${numberOfSteps}`}</span>
|
||||
</div>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<ClaimCertSteps
|
||||
i18nCertText={i18nCertText}
|
||||
isProjectsCompleted={isProjectsCompleted}
|
||||
steps={steps}
|
||||
superBlock={superBlock}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ScrollableAnchor>
|
||||
);
|
||||
};
|
||||
|
||||
CertificationCard.displayName = 'CertStatus';
|
||||
|
||||
export default CertificationCard;
|
@@ -1,88 +0,0 @@
|
||||
import { Link } from 'gatsby';
|
||||
import React from 'react';
|
||||
import { withTranslation, useTranslation } from 'react-i18next';
|
||||
import { SuperBlocks } from '../../../../../config/certification-settings';
|
||||
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
|
||||
import GreenPass from '../../../assets/icons/green-pass';
|
||||
import { Steps } from '../../../redux/prop-types';
|
||||
|
||||
const mapIconStyle = { height: '15px', marginRight: '10px', width: '15px' };
|
||||
|
||||
interface ClaimCertStepsProps {
|
||||
i18nCertText: string;
|
||||
isProjectsCompleted: boolean;
|
||||
steps: Steps;
|
||||
superBlock: SuperBlocks;
|
||||
}
|
||||
|
||||
const ClaimCertSteps = ({
|
||||
isProjectsCompleted,
|
||||
i18nCertText,
|
||||
steps,
|
||||
superBlock
|
||||
}: ClaimCertStepsProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const renderCheckMark = (isCompleted: boolean) => {
|
||||
return isCompleted ? (
|
||||
<GreenPass style={mapIconStyle} />
|
||||
) : (
|
||||
<GreenNotCompleted style={mapIconStyle} />
|
||||
);
|
||||
};
|
||||
|
||||
const settingsLink = '/settings#privacy-settings';
|
||||
const honestyPolicyAnchor = '/settings#honesty-policy';
|
||||
const {
|
||||
isHonest = false,
|
||||
isShowName = false,
|
||||
isShowCerts = false,
|
||||
isShowProfile = false
|
||||
} = steps;
|
||||
|
||||
return (
|
||||
<ul className='map-challenges-ul' data-cy='claim-cert-steps'>
|
||||
<li className='map-challenge-title map-challenge-wrap'>
|
||||
<Link to={honestyPolicyAnchor}>
|
||||
<span className='badge map-badge'>{renderCheckMark(isHonest)}</span>
|
||||
{t('certification-card.accept-honesty')}
|
||||
</Link>
|
||||
</li>
|
||||
<li className='map-challenge-title map-challenge-wrap'>
|
||||
<a href={`#${superBlock}-projects`}>
|
||||
<span className='badge map-badge'>
|
||||
{renderCheckMark(isProjectsCompleted)}
|
||||
</span>
|
||||
{t('certification-card.complete-project', {
|
||||
i18nCertText
|
||||
})}
|
||||
</a>
|
||||
</li>
|
||||
<li className='map-challenge-title map-challenge-wrap'>
|
||||
<Link to={settingsLink}>
|
||||
<span className='badge map-badge'>
|
||||
{renderCheckMark(isShowProfile)}
|
||||
</span>
|
||||
{t('certification-card.set-profile-public')}
|
||||
</Link>
|
||||
</li>
|
||||
<li className='map-challenge-title map-challenge-wrap'>
|
||||
<Link to={settingsLink}>
|
||||
<span className='badge map-badge'>
|
||||
{renderCheckMark(isShowCerts)}
|
||||
</span>
|
||||
{t('certification-card.set-certs-public')}
|
||||
</Link>
|
||||
</li>
|
||||
<li className='map-challenge-title map-challenge-wrap'>
|
||||
<Link to={settingsLink}>
|
||||
<span className='badge map-badge'>{renderCheckMark(isShowName)}</span>
|
||||
{t('certification-card.set-name')}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
ClaimCertSteps.displayName = 'ClaimCertSteps';
|
||||
|
||||
export default withTranslation()(ClaimCertSteps);
|
@@ -1,10 +1,8 @@
|
||||
import cookies from 'browser-cookies';
|
||||
import envData from '../../../config/env.json';
|
||||
import { FlashMessageArg } from '../components/Flash/redux';
|
||||
|
||||
import type {
|
||||
ChallengeFile,
|
||||
ClaimedCertifications,
|
||||
CompletedChallenge,
|
||||
User
|
||||
} from '../redux/prop-types';
|
||||
@@ -25,6 +23,8 @@ function getCSRFToken() {
|
||||
return token ?? '';
|
||||
}
|
||||
|
||||
// TODO: Might want to handle flash messages as close to the request as possible
|
||||
// to make use of the Response object (message, status, etc)
|
||||
async function get<T>(path: string): Promise<T> {
|
||||
return fetch(`${base}${path}`, defaultOptions).then<T>(res => res.json());
|
||||
}
|
||||
@@ -160,31 +160,6 @@ export function getUsernameExists(username: string): Promise<boolean> {
|
||||
return get(`/api/users/exists?username=${username}`);
|
||||
}
|
||||
|
||||
export interface GetVerifyCanClaimCert {
|
||||
response: {
|
||||
type: string;
|
||||
message: {
|
||||
status: boolean;
|
||||
result: string;
|
||||
};
|
||||
variables: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
isCertMap: ClaimedCertifications;
|
||||
completedChallenges: CompletedChallenge[];
|
||||
message?: FlashMessageArg;
|
||||
}
|
||||
|
||||
export function getVerifyCanClaimCert(
|
||||
username: string,
|
||||
certification: string
|
||||
): Promise<GetVerifyCanClaimCert> {
|
||||
return get(
|
||||
`/certificate/verify-can-claim-cert?username=${username}&superBlock=${certification}`
|
||||
);
|
||||
}
|
||||
|
||||
/** POST **/
|
||||
|
||||
interface Donation {
|
||||
|
Reference in New Issue
Block a user