mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-03-06 06:39:18 -05:00
feat(client): flash when can claim cert (#62594)
This commit is contained in:
@@ -1007,6 +1007,7 @@
|
||||
"not-eligible": "This user is not eligible for freeCodeCamp.org certifications at this time.",
|
||||
"profile-private": "{{username}} has chosen to make their profile private. They will need to make their profile public in order for others to be able to view their certification.",
|
||||
"certs-private": "{{username}} has chosen to make their certifications private. They will need to make their certifications public in order for others to be able to view them.",
|
||||
"certs-claimable": "You can now claim the {{certName}} certification! Visit your settings page to claim your certification.",
|
||||
"not-honest": "{{username}} has not yet agreed to our Academic Honesty Pledge.",
|
||||
"user-not-certified": "It looks like user {{username}} is not {{cert}} certified",
|
||||
"invalid-challenge": "That does not appear to be a valid challenge submission",
|
||||
|
||||
@@ -5,6 +5,7 @@ export enum FlashMessages {
|
||||
CertClaimSuccess = 'flash.cert-claim-success',
|
||||
CertificateMissing = 'flash.certificate-missing',
|
||||
CertsPrivate = 'flash.certs-private',
|
||||
CertsClaimable = 'flash.certs-claimable',
|
||||
ChallengeSaveTooBig = 'flash.challenge-save-too-big',
|
||||
ChallengeSubmitTooBig = 'flash.challenge-submit-too-big',
|
||||
CodeSaved = 'flash.code-saved',
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
claimableCertsSelector,
|
||||
userFetchStateSelector
|
||||
} from '../../redux/selectors';
|
||||
import { createFlashMessage } from '../Flash/redux';
|
||||
import { FlashMessages } from '../Flash/redux/flash-messages';
|
||||
|
||||
export function useClaimableCertsNotification() {
|
||||
const dispatch = useDispatch();
|
||||
const claimableCerts = useSelector<unknown, { certTitle: string }[]>(
|
||||
claimableCertsSelector
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { complete } = useSelector(userFetchStateSelector);
|
||||
const hasShownNotification = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!complete || hasShownNotification.current) return;
|
||||
if (claimableCerts.length === 0) return;
|
||||
|
||||
hasShownNotification.current = true;
|
||||
|
||||
// Only one cert is shown as claimable at a time
|
||||
const certName = claimableCerts.at(0)!.certTitle;
|
||||
|
||||
dispatch(
|
||||
createFlashMessage({
|
||||
type: 'info',
|
||||
message: FlashMessages.CertsClaimable,
|
||||
variables: { certName }
|
||||
})
|
||||
);
|
||||
}, [claimableCerts, complete, dispatch]);
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import Testimonials from '../components/landing/components/testimonials';
|
||||
import Certifications from '../components/landing/components/certifications';
|
||||
import Faq from '../components/landing/components/faq';
|
||||
import Benefits from '../components/landing/components/benefits';
|
||||
import { useClaimableCertsNotification } from '../components/helpers/use-claimable-certs-notification';
|
||||
|
||||
import '../components/landing/landing.css';
|
||||
|
||||
const Landing = () => (
|
||||
@@ -27,6 +29,8 @@ const Landing = () => (
|
||||
function IndexPage(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const growthbook = useGrowthBook();
|
||||
useClaimableCertsNotification();
|
||||
|
||||
if (growthbook && growthbook.ready) {
|
||||
growthbook.getFeatureValue('landing-aa-test', false);
|
||||
return (
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '../redux/selectors';
|
||||
|
||||
import callGA from '../analytics/call-ga';
|
||||
import { useClaimableCertsNotification } from '../components/helpers/use-claimable-certs-notification';
|
||||
|
||||
interface FetchState {
|
||||
pending: boolean;
|
||||
@@ -70,6 +71,7 @@ function LearnPage({
|
||||
const { name, completedChallengeCount, isDonating } = user ?? EMPTY_USER;
|
||||
|
||||
const { t } = useTranslation();
|
||||
useClaimableCertsNotification();
|
||||
|
||||
const slug = challengeNode?.challenge?.fields?.slug || '';
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { liveCerts } from '../../config/cert-and-project-map';
|
||||
import {
|
||||
certTypeIdMap,
|
||||
certTypeTitleMap
|
||||
} from '../../../shared-dist/config/certification-settings.js';
|
||||
|
||||
import { randomBetween } from '../utils/random-between';
|
||||
import { getSessionChallengeData } from '../utils/session-storage';
|
||||
@@ -218,3 +223,49 @@ 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 claimableCertsSelector = createSelector([userSelector], user => {
|
||||
if (!user) return [];
|
||||
|
||||
const completedChallengeIds = (user.completedChallenges || []).map(
|
||||
({ id }) => id
|
||||
);
|
||||
|
||||
const isClaimedById = Object.entries(certTypeIdMap).reduce(
|
||||
(acc, [userFlag, certId]) => {
|
||||
acc[certId] = Boolean(user[userFlag]);
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
// Invert certTypeIdMap ({[userFlag]: certId} => {[certId]: userFlag}) to get certType from id
|
||||
const invertedCertTypeIdMap = Object.entries(certTypeIdMap).reduce(
|
||||
(acc, [userFlag, certId]) => {
|
||||
acc[certId] = userFlag;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const claimable = [];
|
||||
|
||||
for (const { id, projects } of liveCerts) {
|
||||
if (!projects) continue;
|
||||
if (isClaimedById[id]) continue;
|
||||
|
||||
const projectIds = projects.map(p => p.id);
|
||||
const allProjectsComplete = projectIds.every(id =>
|
||||
completedChallengeIds.includes(id)
|
||||
);
|
||||
|
||||
const certType = invertedCertTypeIdMap[id];
|
||||
const certTitle = certTypeTitleMap[certType];
|
||||
if (allProjectsComplete) {
|
||||
claimable.push({
|
||||
certTitle
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return claimable;
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ const toneUrls = {
|
||||
[FlashMessages.CertClaimSuccess]:
|
||||
'https://campfire-mode.freecodecamp.org/cert.mp3',
|
||||
[FlashMessages.CertificateMissing]: TRY_AGAIN,
|
||||
[FlashMessages.CertsClaimable]: CHAL_COMP,
|
||||
[FlashMessages.CertsPrivate]: TRY_AGAIN,
|
||||
[FlashMessages.ChallengeSaveTooBig]: TRY_AGAIN,
|
||||
[FlashMessages.ChallengeSubmitTooBig]: TRY_AGAIN,
|
||||
|
||||
Reference in New Issue
Block a user