chore(client): fix several type errors (#58500)

This commit is contained in:
Oliver Eyton-Williams
2025-01-31 17:25:43 +01:00
committed by GitHub
parent acfabb69de
commit 1738b1f05f
20 changed files with 147 additions and 99 deletions
@@ -1,11 +1,15 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import type { User } from '../../../redux/prop-types';
import { Link, AvatarRenderer } from '../../helpers';
import Login from './login';
interface AuthOrProfileProps {
user?: User;
user?: {
isDonating: boolean;
username: string;
picture: string;
yearsTopContributor: string[];
};
}
const AuthOrProfile = ({ user }: AuthOrProfileProps): JSX.Element => {
const { t } = useTranslation();
@@ -2,7 +2,6 @@ import React, { RefObject } from 'react';
import { useTranslation } from 'react-i18next';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faBars } from '@fortawesome/free-solid-svg-icons';
import { User } from '../../../redux/prop-types';
interface MenuButtonProps {
className?: string;
@@ -10,7 +9,6 @@ interface MenuButtonProps {
innerRef?: RefObject<HTMLButtonElement>;
showMenu: () => void;
hideMenu: () => void;
user?: User;
}
const MenuButton = ({
@@ -13,14 +13,13 @@ import { openSignoutModal, toggleTheme } from '../../../redux/actions';
import { Link } from '../../helpers';
import { LocalStorageThemes } from '../../../redux/types';
import { themeSelector } from '../../../redux/selectors';
import { User } from '../../../redux/prop-types';
import SupporterBadge from '../../../assets/icons/supporter-badge';
export interface NavLinksProps {
displayMenu: boolean;
showMenu: () => void;
hideMenu: () => void;
user?: User;
user?: { isDonating: boolean; username: string };
menuButtonRef: React.RefObject<HTMLButtonElement>;
openSignoutModal: () => void;
theme: LocalStorageThemes;
@@ -7,7 +7,7 @@ import { Link, SkeletonSprite } from '../../helpers';
import { SEARCH_EXPOSED_WIDTH } from '../../../../config/misc';
import FreeCodeCampLogo from '../../../assets/icons/freecodecamp-logo';
import MenuButton from './menu-button';
import NavLinks, { type NavLinksProps } from './nav-links';
import NavLinks from './nav-links';
import AuthOrProfile from './auth-or-profile';
import LanguageList from './language-list';
@@ -18,10 +18,17 @@ const SearchBarOptimized = Loadable(
() => import('../../search/searchBar/search-bar-optimized')
);
type UniversalNavProps = Omit<
NavLinksProps,
'toggleTheme' | 'openSignoutModal'
> & {
type UniversalNavProps = {
displayMenu: boolean;
showMenu: () => void;
hideMenu: () => void;
menuButtonRef: React.RefObject<HTMLButtonElement>;
user: {
isDonating: boolean;
username: string;
picture: string;
yearsTopContributor: string[];
};
fetchState: { pending: boolean };
searchBarRef?: React.RefObject<HTMLDivElement>;
pathname: string;
@@ -81,7 +88,6 @@ const UniversalNav = ({
hideMenu={hideMenu}
innerRef={menuButtonRef}
showMenu={showMenu}
user={user}
/>
{!isSearchExposedWidth && search}
<NavLinks
+40 -47
View File
@@ -1,24 +1,46 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import React from 'react';
import { create, ReactTestRendererJSON } from 'react-test-renderer';
import AuthOrProfile from './components/auth-or-profile';
const defaultUserProps = {
user: {
username: 'test-user',
picture: 'https://freecodecamp.org/image.png',
isDonating: false,
yearsTopContributor: []
},
pending: false,
pathName: '/learn'
};
const donatingUserProps = {
...defaultUserProps,
user: {
...defaultUserProps.user,
isDonating: true
}
};
const topContributorUserProps = {
...defaultUserProps,
user: {
...defaultUserProps.user,
yearsTopContributor: ['2020']
}
};
const topDonatingContributorUserProps = {
...topContributorUserProps,
user: {
...topContributorUserProps.user,
isDonating: true
}
};
jest.mock('../../analytics');
describe('<AuthOrProfile />', () => {
it('has avatar with default border for default users', () => {
const defaultUserProps = {
user: {
username: 'test-user',
picture: 'https://freecodecamp.org/image.png'
},
pending: false,
pathName: '/learn'
};
const componentTree = create(
<AuthOrProfile {...defaultUserProps} />
).toJSON();
@@ -26,16 +48,6 @@ describe('<AuthOrProfile />', () => {
});
it('has avatar with gold border for donating users', () => {
const donatingUserProps = {
user: {
username: 'test-user',
picture: 'https://freecodecamp.org/image.png',
isDonating: true
},
pending: false,
pathName: '/learn'
};
const componentTree = create(
<AuthOrProfile {...donatingUserProps} />
).toJSON();
@@ -43,16 +55,6 @@ describe('<AuthOrProfile />', () => {
});
it('has avatar with blue border for top contributors', () => {
const topContributorUserProps = {
user: {
username: 'test-user',
picture: 'https://freecodecamp.org/image.png',
yearsTopContributor: [2020]
},
pending: false,
pathName: '/learn'
};
const componentTree = create(
<AuthOrProfile {...topContributorUserProps} />
).toJSON();
@@ -60,17 +62,6 @@ describe('<AuthOrProfile />', () => {
});
it('has avatar with purple border for donating top contributors', () => {
const topDonatingContributorUserProps = {
user: {
username: 'test-user',
picture: 'https://freecodecamp.org/image.png',
isDonating: true,
yearsTopContributor: [2020]
},
pending: false,
pathName: '/learn'
};
const componentTree = create(
<AuthOrProfile {...topDonatingContributorUserProps} />
).toJSON();
@@ -78,15 +69,17 @@ describe('<AuthOrProfile />', () => {
});
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const profileNavItem = (component: any) => component.children[0];
type Component = {
children: { props: { className: string } }[];
};
const profileNavItem = (component: Component) => component.children[0];
const avatarHasClass = (
componentTree: ReactTestRendererJSON | ReactTestRendererJSON[] | null,
classes: string
) => {
return (
profileNavItem(componentTree).props.className ===
profileNavItem(componentTree as unknown as Component).props.className ===
'avatar-container ' + classes
);
};
+6 -2
View File
@@ -5,7 +5,6 @@
import React from 'react';
import { ConnectedProps, connect } from 'react-redux';
import { createSelector } from 'reselect';
import { User } from '../../redux/prop-types';
import { examInProgressSelector } from '../../redux/selectors';
import UniversalNav from './components/universal-nav';
@@ -26,7 +25,12 @@ type PropsFromRedux = ConnectedProps<typeof connector>;
type Props = PropsFromRedux & {
fetchState: { pending: boolean };
user: User;
user: {
isDonating: boolean;
username: string;
picture: string;
yearsTopContributor: string[];
};
skipButtonText: string;
pathname: string;
};
-1
View File
@@ -57,7 +57,6 @@ const superBlockHeadings: { [key in SuperBlockStage]: string } = {
[SuperBlockStage.Professional]: 'landing.professional-certs-heading',
[SuperBlockStage.Extra]: 'landing.interview-prep-heading',
[SuperBlockStage.Legacy]: 'landing.legacy-curriculum-heading',
[SuperBlockStage.New]: '', // TODO: add translation
[SuperBlockStage.Next]: 'landing.next-heading',
[SuperBlockStage.NextEnglish]: 'landing.next-english-heading',
[SuperBlockStage.Upcoming]: 'landing.upcoming-heading'
@@ -202,7 +202,10 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
});
}
getUrlValidation(url: string) {
getUrlValidation(url: string): {
state: 'success' | 'warning' | 'error';
message: string;
} {
const { t } = this.props;
const len = url.length;
@@ -1 +1 @@
export { parseDate, formatYears } from './utils';
export { parseDate } from './utils';
@@ -90,6 +90,11 @@ function renderWithRedux(ui: JSX.Element) {
}
describe('<Profile/>', () => {
it('renders the report button on another persons profile', () => {
// TODO: Profile is a mess, it shouldn't depend on the entire user. Each
// component Camper, Stats, HeatMap etc should be get the relevant data from
// the store themselves.
// @ts-expect-error - quick hack to mollify TS.
renderWithRedux(<Profile {...notMyProfileProps} />);
const reportButton: HTMLElement = screen.getByText('buttons.flag-user');
@@ -97,6 +102,7 @@ describe('<Profile/>', () => {
});
it('renders correctly', () => {
// @ts-expect-error - quick hack to mollify TS.
const { container } = renderWithRedux(<Profile {...notMyProfileProps} />);
expect(container).toMatchSnapshot();
@@ -68,6 +68,7 @@ function DeleteModal(props: DeleteModalProps): JSX.Element {
/>
</FormGroup>
<Spacer size='xs' />
{/* @ts-expect-error The UI lib's types don't allow this: https://github.com/freeCodeCamp/ui/issues/473 */}
<Button
block={true}
size='large'
@@ -66,6 +66,7 @@ function ResetModal(props: ResetModalProps): JSX.Element {
/>
</FormGroup>
<Spacer size='xs' />
{/* @ts-expect-error freecodecamp/ui doesn't allow disable to be false: https://github.com/freeCodeCamp/ui/issues/473 */}
<Button
block={true}
size='large'
-15
View File
@@ -152,21 +152,6 @@ export interface PrerequisiteChallenge {
slug?: string;
}
export type ExtendedChallenge = {
block: string;
challengeType: number;
dashedName: string;
fields: {
slug: string;
};
id: string;
isCompleted: boolean;
order: number;
superBlock: SuperBlocks;
stepNumber: number;
title: string;
};
export type ChallengeNode = {
challenge: {
block: string;
@@ -14,7 +14,6 @@ import { isAuditedSuperBlock } from '../../../../../shared/utils/is-audited';
import Caret from '../../../assets/icons/caret';
import { Link } from '../../../components/helpers';
import { completedChallengesSelector } from '../../../redux/selectors';
import { ChallengeNode, CompletedChallenge } from '../../../redux/prop-types';
import { playTone } from '../../../utils/tone';
import { makeExpandedBlockSelector, toggleBlock } from '../redux';
import { isGridBased, isProjectBased } from '../../../utils/curriculum-layout';
@@ -30,15 +29,13 @@ import './block.css';
const { curriculumLocale } = envData;
type Challenge = ChallengeNode['challenge'];
const mapStateToProps = (state: unknown, ownProps: { block: string }) => {
const expandedSelector = makeExpandedBlockSelector(ownProps.block);
return createSelector(
expandedSelector,
completedChallengesSelector,
(isExpanded: boolean, completedChallenges: CompletedChallenge[]) => ({
(isExpanded: boolean, completedChallenges: { id: string }[]) => ({
isExpanded,
completedChallengeIds: completedChallenges.map(({ id }) => id)
})
@@ -48,10 +45,20 @@ const mapStateToProps = (state: unknown, ownProps: { block: string }) => {
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators({ toggleBlock }, dispatch);
interface ChallengeInfo {
id: string;
title: string;
fields: { slug: string };
dashedName: string;
challengeType: number;
blockLayout: BlockLayouts;
superBlock: SuperBlocks;
}
interface BlockProps {
block: string;
blockType: BlockTypes | null;
challenges: Challenge[];
challenges: ChallengeInfo[];
completedChallengeIds: string[];
isExpanded: boolean;
superBlock: SuperBlocks;
@@ -124,10 +131,10 @@ class Block extends Component<BlockProps> {
const expandText = t('intro:misc-text.expand');
const collapseText = t('intro:misc-text.collapse');
const isBlockCompleted = completedCount === extendedChallenges.length;
const isBlockCompleted = completedCount === challenges.length;
const percentageCompleted = Math.floor(
(completedCount / extendedChallenges.length) * 100
(completedCount / challenges.length) * 100
);
// since the Blocks are not components, we need link to exist even if it's
@@ -142,7 +149,7 @@ class Block extends Component<BlockProps> {
if (completedCount === 0) {
return t('learn.not-started');
}
if (completedCount === extendedChallenges.length) {
if (isBlockCompleted) {
return t('learn.completed');
}
return `${percentageCompleted}% ${t('learn.completed')}`;
@@ -188,12 +195,12 @@ class Block extends Component<BlockProps> {
<span
aria-hidden='true'
className='map-completed-count'
>{`${completedCount}/${extendedChallenges.length}`}</span>
>{`${completedCount}/${challenges.length}`}</span>
<span className='sr-only'>
,{' '}
{t('learn.challenges-completed', {
completedCount,
totalChallenges: extendedChallenges.length
totalChallenges: challenges.length
})}
</span>
</div>
@@ -3,14 +3,23 @@ import { withTranslation, useTranslation } from 'react-i18next';
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
import GreenPass from '../../../assets/icons/green-pass';
import { ExtendedChallenge } from '../../../redux/prop-types';
import { SuperBlocks } from '../../../../../shared/config/curriculum';
import { challengeTypes } from '../../../../../shared/config/challenge-types';
import { Link } from '../../../components/helpers';
import { ButtonLink } from '../../../components/helpers/button-link';
interface ChallengeInfo {
isCompleted: boolean;
fields: { slug: string };
dashedName: string;
title: string;
stepNumber: number;
superBlock: SuperBlocks;
challengeType: number;
}
interface Challenges {
challenges: ExtendedChallenge[];
challenges: ChallengeInfo[];
isProjectBlock: boolean;
isGridMap?: boolean;
blockTitle?: string | null;
@@ -19,7 +28,7 @@ interface Challenges {
const CheckMark = ({ isCompleted }: { isCompleted: boolean }) =>
isCompleted ? <GreenPass /> : <GreenNotCompleted />;
const ListChallenge = ({ challenge }: { challenge: ExtendedChallenge }) => (
const ListChallenge = ({ challenge }: { challenge: ChallengeInfo }) => (
<Link to={challenge.fields.slug}>
<span className='map-badge'>
<CheckMark isCompleted={challenge.isCompleted} />
@@ -28,7 +37,7 @@ const ListChallenge = ({ challenge }: { challenge: ExtendedChallenge }) => (
</Link>
);
const CertChallenge = ({ challenge }: { challenge: ExtendedChallenge }) => (
const CertChallenge = ({ challenge }: { challenge: ChallengeInfo }) => (
<Link to={challenge.fields.slug}>
{challenge.title}
<span className='map-badge map-project-checkmark'>
@@ -38,7 +47,7 @@ const CertChallenge = ({ challenge }: { challenge: ExtendedChallenge }) => (
);
// Step or Task challenge
const GridChallenge = ({ challenge }: { challenge: ExtendedChallenge }) => {
const GridChallenge = ({ challenge }: { challenge: ChallengeInfo }) => {
const { t } = useTranslation();
return (
@@ -3,13 +3,13 @@ import { useTranslation } from 'react-i18next';
// TODO: Add this component to freecodecamp/ui and remove this dependency
import { Disclosure } from '@headlessui/react';
import { ChallengeNode } from '../../../redux/prop-types';
import { SuperBlocks } from '../../../../../shared/config/curriculum';
import DropDown from '../../../assets/icons/dropdown';
// TODO: See if there's a nice way to incorporate the structure into data Gatsby
// sources from the curriculum, rather than importing it directly.
import superBlockStructure from '../../../../../curriculum/superblock-structure/full-stack.json';
import { ChapterIcon } from '../../../assets/chapter-icon';
import { BlockLayouts, BlockTypes } from '../../../../../shared/config/blocks';
import { FsdChapters } from '../../../../../shared/config/chapters';
import envData from '../../../../config/env.json';
import Block from './block';
@@ -34,8 +34,21 @@ interface ModuleProps {
totalSteps: number;
completedSteps: number;
}
interface Challenge {
id: string;
block: string;
blockType: BlockTypes;
title: string;
fields: { slug: string };
dashedName: string;
challengeType: number;
blockLayout: BlockLayouts;
superBlock: SuperBlocks;
}
interface SuperBlockTreeViewProps {
challenges: ChallengeNode['challenge'][];
challenges: Challenge[];
superBlock: SuperBlocks;
chosenBlock: string;
completedChallengeIds: string[];
@@ -191,7 +204,7 @@ const LinkBlock = ({
challenges
}: {
superBlock: SuperBlocks;
challenges?: ChallengeNode['challenge'][];
challenges?: Challenge[];
}) =>
challenges?.length ? (
<li className='link-block'>
@@ -24,9 +24,10 @@ import {
userFetchStateSelector,
signInLoadingSelector
} from '../../redux/selectors';
import type { ChallengeNode, User } from '../../redux/prop-types';
import type { User } from '../../redux/prop-types';
import { CertTitle, liveCerts } from '../../../config/cert-and-project-map';
import { superBlockToCertMap } from '../../../../shared/config/certification-settings';
import { BlockLayouts, BlockTypes } from '../../../../shared/config/blocks';
import Block from './components/block';
import CertChallenge from './components/cert-challenge';
import LegacyLinks from './components/legacy-links';
@@ -43,6 +44,23 @@ type FetchState = {
errored: boolean;
};
type ChallengeNode = {
challenge: {
fields: { slug: string; blockName: string };
id: string;
block: string;
blockType: BlockTypes;
challengeType: number;
title: string;
order: number;
superBlock: SuperBlocks;
dashedName: string;
blockLayout: BlockLayouts;
chapter: string;
module: string;
};
};
type SuperBlockProps = {
currentChallengeId: string;
data: {
+1
View File
@@ -3,6 +3,7 @@ declare global {
// This is a feature Gatsby adds to the `window` object.
// https://github.com/gatsbyjs/gatsby/blob/deb41cdfefbefe0c170b5dd7c10a19ba2b338f6e/packages/gatsby/cache-dir/production-app.js#L28
___loader: {
enqueue: () => void;
hovering: (path: string | null) => void;
};
}
@@ -31,6 +31,7 @@ function getComponentNameAndProps(
const LayoutReactComponent = layoutSelector({
element: { type: elementType, props: {}, key: '' },
props: {
data: {},
location: {
pathname
},
+1 -1
View File
@@ -112,7 +112,7 @@ export const hiddenLangs = [Languages.Korean];
/**
* This array contains languages that use the RTL layouts.
*/
export const rtlLangs = [];
export const rtlLangs: Languages[] = [];
// locale is sourced from a JSON file, so we use getLangCode to
// find the associated enum values