feat(client): ts-migrate multiple files (#43262)
* feat(client): ts-migrate rename files * feat(client): ts-migrate client/src/templates/Introduction/* * feat(client): ts-migrate client/src/components/formHelpers/form* * fix: import * Update client/src/components/formHelpers/form-validators.tsx Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * Update client/src/components/formHelpers/form-fields.tsx Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * Update client/src/components/formHelpers/form-fields.tsx Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> * fix: types in client/src/components/formHelpers/index.tsx * fix: types in client/src/templates/Introduction/super-block-intro.tsx * fix: types in client/src/components/formHelpers/* * fix: signInLoading and value types * Update client/src/templates/Introduction/super-block-intro.tsx Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * Update client/src/components/formHelpers/index.tsx Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * Update client/src/components/formHelpers/index.tsx Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * Update client/src/components/formHelpers/index.tsx Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> * chore(deps): update dependency rollup to v2.58.1 * fix: rename variables and fix imports for ts-migrate * fix: remove `Type` suffix from the type definition. * Update client/src/components/formHelpers/form.tsx Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Renovate Bot <bot@renovateapp.com>
This commit is contained in:
committed by
GitHub
parent
001aa3ea9e
commit
9abc5f66ba
@ -40,7 +40,7 @@ const mapDispatchToProps = (dispatch: Dispatch<AnyAction>) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
type DonateModalProps = {
|
type DonateModalProps = {
|
||||||
activeDonors: number;
|
activeDonors?: number;
|
||||||
closeDonationModal: typeof closeDonationModal;
|
closeDonationModal: typeof closeDonationModal;
|
||||||
executeGA: typeof executeGA;
|
executeGA: typeof executeGA;
|
||||||
location: WindowLocation | undefined;
|
location: WindowLocation | undefined;
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { Form } from 'react-final-form';
|
|
||||||
|
|
||||||
import {
|
|
||||||
FormFields,
|
|
||||||
BlockSaveButton,
|
|
||||||
BlockSaveWrapper,
|
|
||||||
formatUrlValues
|
|
||||||
} from './';
|
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
buttonText: PropTypes.string,
|
|
||||||
enableSubmit: PropTypes.bool,
|
|
||||||
formFields: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({ name: PropTypes.string, label: PropTypes.string })
|
|
||||||
.isRequired
|
|
||||||
).isRequired,
|
|
||||||
hideButton: PropTypes.bool,
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
initialValues: PropTypes.object,
|
|
||||||
options: PropTypes.shape({
|
|
||||||
ignored: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
isEditorLinkAllowed: PropTypes.bool,
|
|
||||||
required: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
types: PropTypes.objectOf(PropTypes.string),
|
|
||||||
placeholders: PropTypes.shape({
|
|
||||||
solution: PropTypes.string,
|
|
||||||
githubLink: PropTypes.string
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
submit: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
function DynamicForm({
|
|
||||||
id,
|
|
||||||
formFields,
|
|
||||||
initialValues,
|
|
||||||
options,
|
|
||||||
submit,
|
|
||||||
buttonText,
|
|
||||||
enableSubmit,
|
|
||||||
hideButton
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
initialValues={initialValues}
|
|
||||||
onSubmit={(values, ...args) =>
|
|
||||||
submit(formatUrlValues(values, options), ...args)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{({ handleSubmit, pristine, error }) => (
|
|
||||||
<form
|
|
||||||
id={`dynamic-${id}`}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
>
|
|
||||||
<FormFields formFields={formFields} options={options} />
|
|
||||||
<BlockSaveWrapper>
|
|
||||||
{hideButton ? null : (
|
|
||||||
<BlockSaveButton disabled={(pristine && !enableSubmit) || error}>
|
|
||||||
{buttonText ? buttonText : null}
|
|
||||||
</BlockSaveButton>
|
|
||||||
)}
|
|
||||||
</BlockSaveWrapper>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DynamicForm.displayName = 'DynamicForm';
|
|
||||||
DynamicForm.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default DynamicForm;
|
|
@ -1,7 +1,7 @@
|
|||||||
import { render, fireEvent, screen } from '@testing-library/react';
|
import { render, fireEvent, screen } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Form from './Form';
|
import Form from './form';
|
||||||
|
|
||||||
const defaultTestProps = {
|
const defaultTestProps = {
|
||||||
buttonText: 'Submit',
|
buttonText: 'Submit',
|
||||||
|
@ -7,7 +7,7 @@ const style = {
|
|||||||
function BlockSaveWrapper({
|
function BlockSaveWrapper({
|
||||||
children
|
children
|
||||||
}: {
|
}: {
|
||||||
children?: React.ReactElement;
|
children?: React.ReactElement | null;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return <div style={style}>{children}</div>;
|
return <div style={style}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
@ -8,35 +8,26 @@ import {
|
|||||||
} from '@freecodecamp/react-bootstrap';
|
} from '@freecodecamp/react-bootstrap';
|
||||||
import { kebabCase } from 'lodash-es';
|
import { kebabCase } from 'lodash-es';
|
||||||
import normalizeUrl from 'normalize-url';
|
import normalizeUrl from 'normalize-url';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Field } from 'react-final-form';
|
import { Field } from 'react-final-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FormOptions } from './form';
|
||||||
import {
|
import {
|
||||||
editorValidator,
|
editorValidator,
|
||||||
localhostValidator,
|
localhostValidator,
|
||||||
composeValidators,
|
composeValidators,
|
||||||
fCCValidator,
|
fCCValidator,
|
||||||
httpValidator
|
httpValidator
|
||||||
} from './FormValidators';
|
} from './form-validators';
|
||||||
|
|
||||||
const propTypes = {
|
type FormFieldsProps = {
|
||||||
formFields: PropTypes.arrayOf(
|
formFields: { name: string; label: string }[];
|
||||||
PropTypes.shape({ name: PropTypes.string, label: PropTypes.string })
|
options: FormOptions;
|
||||||
.isRequired
|
|
||||||
).isRequired,
|
|
||||||
options: PropTypes.shape({
|
|
||||||
ignored: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
isEditorLinkAllowed: PropTypes.bool,
|
|
||||||
placeholders: PropTypes.objectOf(PropTypes.string),
|
|
||||||
required: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
types: PropTypes.objectOf(PropTypes.string)
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function FormFields(props) {
|
function FormFields(props: FormFieldsProps): JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { formFields, options = {} } = props;
|
const { formFields, options = {} }: FormFieldsProps = props;
|
||||||
const {
|
const {
|
||||||
ignored = [],
|
ignored = [],
|
||||||
placeholders = {},
|
placeholders = {},
|
||||||
@ -46,13 +37,18 @@ function FormFields(props) {
|
|||||||
isLocalLinkAllowed = false
|
isLocalLinkAllowed = false
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
const nullOrWarning = (value, error, isURL, name) => {
|
const nullOrWarning = (
|
||||||
let validationError;
|
value: string,
|
||||||
|
error: unknown,
|
||||||
|
isURL: boolean,
|
||||||
|
name: string
|
||||||
|
) => {
|
||||||
|
let validationError: string | undefined;
|
||||||
if (value && isURL) {
|
if (value && isURL) {
|
||||||
try {
|
try {
|
||||||
normalizeUrl(value, { stripWWW: false });
|
normalizeUrl(value, { stripWWW: false });
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
validationError = err.message;
|
validationError = (err as { message?: string })?.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const validationWarning = composeValidators(
|
const validationWarning = composeValidators(
|
||||||
@ -61,7 +57,9 @@ function FormFields(props) {
|
|||||||
httpValidator,
|
httpValidator,
|
||||||
isLocalLinkAllowed ? null : localhostValidator
|
isLocalLinkAllowed ? null : localhostValidator
|
||||||
)(value);
|
)(value);
|
||||||
const message = error || validationError || validationWarning;
|
const message: string = (error ||
|
||||||
|
validationError ||
|
||||||
|
validationWarning) as string;
|
||||||
return message ? (
|
return message ? (
|
||||||
<HelpBlock>
|
<HelpBlock>
|
||||||
<Alert
|
<Alert
|
||||||
@ -78,6 +76,7 @@ function FormFields(props) {
|
|||||||
{formFields
|
{formFields
|
||||||
.filter(formField => !ignored.includes(formField.name))
|
.filter(formField => !ignored.includes(formField.name))
|
||||||
.map(({ name, label }) => (
|
.map(({ name, label }) => (
|
||||||
|
// TODO: verify if the value is always a string
|
||||||
<Field key={`${kebabCase(name)}-field`} name={name}>
|
<Field key={`${kebabCase(name)}-field`} name={name}>
|
||||||
{({ input: { value, onChange }, meta: { pristine, error } }) => {
|
{({ input: { value, onChange }, meta: { pristine, error } }) => {
|
||||||
const key = kebabCase(name);
|
const key = kebabCase(name);
|
||||||
@ -100,7 +99,7 @@ function FormFields(props) {
|
|||||||
required={required.includes(name)}
|
required={required.includes(name)}
|
||||||
rows={4}
|
rows={4}
|
||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value as string}
|
||||||
/>
|
/>
|
||||||
{nullOrWarning(value, !pristine && error, isURL, name)}
|
{nullOrWarning(value, !pristine && error, isURL, name)}
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@ -114,6 +113,5 @@ function FormFields(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FormFields.displayName = 'FormFields';
|
FormFields.displayName = 'FormFields';
|
||||||
FormFields.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default FormFields;
|
export default FormFields;
|
@ -9,21 +9,26 @@ const fCCRegex =
|
|||||||
const localhostRegex = /localhost:/;
|
const localhostRegex = /localhost:/;
|
||||||
const httpRegex = /http(?!s|([^s]+?localhost))/;
|
const httpRegex = /http(?!s|([^s]+?localhost))/;
|
||||||
|
|
||||||
export const editorValidator = value =>
|
export const editorValidator = (value: string): React.ReactElement | null =>
|
||||||
editorRegex.test(value) ? <Trans>validation.editor-url</Trans> : null;
|
editorRegex.test(value) ? <Trans>validation.editor-url</Trans> : null;
|
||||||
|
|
||||||
export const fCCValidator = value =>
|
export const fCCValidator = (value: string): React.ReactElement | null =>
|
||||||
fCCRegex.test(value) ? <Trans>validation.own-work-url</Trans> : null;
|
fCCRegex.test(value) ? <Trans>validation.own-work-url</Trans> : null;
|
||||||
|
|
||||||
export const localhostValidator = value =>
|
export const localhostValidator = (value: string): React.ReactElement | null =>
|
||||||
localhostRegex.test(value) ? (
|
localhostRegex.test(value) ? (
|
||||||
<Trans>validation.publicly-visible-url</Trans>
|
<Trans>validation.publicly-visible-url</Trans>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
export const httpValidator = value =>
|
export const httpValidator = (value: string): React.ReactElement | null =>
|
||||||
httpRegex.test(value) ? <Trans>validation.http-url</Trans> : null;
|
httpRegex.test(value) ? <Trans>validation.http-url</Trans> : null;
|
||||||
|
|
||||||
export const composeValidators =
|
export type Validator = (value: string) => React.ReactElement | null;
|
||||||
(...validators) =>
|
export function composeValidators(...validators: (Validator | null)[]) {
|
||||||
value =>
|
return (value: string): ReturnType<Validator> | null =>
|
||||||
validators.reduce((error, validator) => error ?? validator?.(value), null);
|
validators.reduce(
|
||||||
|
(error: ReturnType<Validator>, validator) =>
|
||||||
|
error ?? (validator ? validator(value) : null),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
75
client/src/components/formHelpers/form.tsx
Normal file
75
client/src/components/formHelpers/form.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Form } from 'react-final-form';
|
||||||
|
|
||||||
|
import {
|
||||||
|
URLValues,
|
||||||
|
ValidatedValues,
|
||||||
|
FormFields,
|
||||||
|
BlockSaveButton,
|
||||||
|
BlockSaveWrapper,
|
||||||
|
formatUrlValues
|
||||||
|
} from '../formHelpers/index';
|
||||||
|
|
||||||
|
export type FormOptions = {
|
||||||
|
ignored?: string[];
|
||||||
|
isEditorLinkAllowed?: boolean;
|
||||||
|
isLocalLinkAllowed?: boolean;
|
||||||
|
required?: string[];
|
||||||
|
types?: { [key: string]: string };
|
||||||
|
placeholders?: { [key: string]: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormProps = {
|
||||||
|
buttonText?: string;
|
||||||
|
enableSubmit?: boolean;
|
||||||
|
formFields: { name: string; label: string }[];
|
||||||
|
hideButton?: boolean;
|
||||||
|
id?: string;
|
||||||
|
initialValues?: Record<string, unknown>;
|
||||||
|
options: FormOptions;
|
||||||
|
submit: (values: ValidatedValues, ...args: unknown[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function DynamicForm({
|
||||||
|
id,
|
||||||
|
formFields,
|
||||||
|
initialValues,
|
||||||
|
options,
|
||||||
|
submit,
|
||||||
|
buttonText,
|
||||||
|
enableSubmit,
|
||||||
|
hideButton
|
||||||
|
}: FormProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={(values: URLValues, ...args: unknown[]) => {
|
||||||
|
submit(formatUrlValues(values, options), ...args);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ handleSubmit, pristine, error }) => (
|
||||||
|
<form
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
id={`dynamic-${id}`}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
<FormFields formFields={formFields} options={options} />
|
||||||
|
<BlockSaveWrapper>
|
||||||
|
{hideButton ? null : (
|
||||||
|
<BlockSaveButton
|
||||||
|
disabled={(pristine && !enableSubmit) || (error as boolean)}
|
||||||
|
>
|
||||||
|
{buttonText ? buttonText : null}
|
||||||
|
</BlockSaveButton>
|
||||||
|
)}
|
||||||
|
</BlockSaveWrapper>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicForm.displayName = 'DynamicForm';
|
||||||
|
|
||||||
|
export default DynamicForm;
|
@ -1,45 +0,0 @@
|
|||||||
import normalizeUrl from 'normalize-url';
|
|
||||||
import {
|
|
||||||
localhostValidator,
|
|
||||||
editorValidator,
|
|
||||||
composeValidators,
|
|
||||||
fCCValidator,
|
|
||||||
httpValidator
|
|
||||||
} from './FormValidators';
|
|
||||||
|
|
||||||
export { default as BlockSaveButton } from './block-save-button';
|
|
||||||
export { default as BlockSaveWrapper } from './block-save-wrapper';
|
|
||||||
export { default as Form } from './Form.js';
|
|
||||||
export { default as FormFields } from './FormFields.js';
|
|
||||||
|
|
||||||
const normalizeOptions = {
|
|
||||||
stripWWW: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export function formatUrlValues(values, options) {
|
|
||||||
const { isEditorLinkAllowed, isLocalLinkAllowed, types } = options;
|
|
||||||
const validatedValues = { values: {}, errors: [], invalidValues: [] };
|
|
||||||
const urlValues = Object.keys(values).reduce((result, key) => {
|
|
||||||
let value = values[key];
|
|
||||||
const nullOrWarning = composeValidators(
|
|
||||||
fCCValidator,
|
|
||||||
httpValidator,
|
|
||||||
isLocalLinkAllowed ? null : localhostValidator,
|
|
||||||
key === 'githubLink' || isEditorLinkAllowed ? null : editorValidator
|
|
||||||
)(value);
|
|
||||||
if (nullOrWarning) {
|
|
||||||
validatedValues.invalidValues.push(nullOrWarning);
|
|
||||||
}
|
|
||||||
if (value && types[key] === 'url') {
|
|
||||||
try {
|
|
||||||
value = normalizeUrl(value, normalizeOptions);
|
|
||||||
} catch (err) {
|
|
||||||
// Not a valid URL for testing or submission
|
|
||||||
validatedValues.errors.push({ error: err, value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { ...result, [key]: value };
|
|
||||||
}, {});
|
|
||||||
validatedValues.values = urlValues;
|
|
||||||
return validatedValues;
|
|
||||||
}
|
|
70
client/src/components/formHelpers/index.tsx
Normal file
70
client/src/components/formHelpers/index.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import normalizeUrl from 'normalize-url';
|
||||||
|
import { FormOptions } from './form';
|
||||||
|
import {
|
||||||
|
localhostValidator,
|
||||||
|
editorValidator,
|
||||||
|
composeValidators,
|
||||||
|
fCCValidator,
|
||||||
|
httpValidator
|
||||||
|
} from './form-validators';
|
||||||
|
|
||||||
|
export { default as BlockSaveButton } from './block-save-button';
|
||||||
|
export { default as BlockSaveWrapper } from './block-save-wrapper';
|
||||||
|
export { default as Form } from './form';
|
||||||
|
export { default as FormFields } from './form-fields';
|
||||||
|
|
||||||
|
const normalizeOptions = {
|
||||||
|
stripWWW: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export type URLValues = {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ValidationError = {
|
||||||
|
error: { message?: string };
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ValidatedValues = {
|
||||||
|
values: URLValues;
|
||||||
|
errors: ValidationError[];
|
||||||
|
invalidValues: (JSX.Element | null)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatUrlValues(
|
||||||
|
values: URLValues,
|
||||||
|
options: FormOptions
|
||||||
|
): ValidatedValues {
|
||||||
|
const { isEditorLinkAllowed, isLocalLinkAllowed, types } = options;
|
||||||
|
const validatedValues: ValidatedValues = {
|
||||||
|
values: {},
|
||||||
|
errors: [],
|
||||||
|
invalidValues: []
|
||||||
|
};
|
||||||
|
const urlValues = Object.keys(values).reduce((result, key: string) => {
|
||||||
|
let value: string = values[key];
|
||||||
|
const nullOrWarning: JSX.Element | null = composeValidators(
|
||||||
|
fCCValidator,
|
||||||
|
httpValidator,
|
||||||
|
isLocalLinkAllowed ? null : localhostValidator,
|
||||||
|
key === 'githubLink' || isEditorLinkAllowed ? null : editorValidator
|
||||||
|
)(value);
|
||||||
|
if (nullOrWarning) {
|
||||||
|
validatedValues.invalidValues.push(nullOrWarning);
|
||||||
|
}
|
||||||
|
if (value && types && types[key] === 'url') {
|
||||||
|
try {
|
||||||
|
value = normalizeUrl(value, normalizeOptions);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
validatedValues.errors.push({
|
||||||
|
error: err as { message?: string },
|
||||||
|
value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ...result, [key]: value };
|
||||||
|
}, {});
|
||||||
|
validatedValues.values = urlValues;
|
||||||
|
return validatedValues;
|
||||||
|
}
|
@ -8,7 +8,7 @@ import {
|
|||||||
frontEndProject,
|
frontEndProject,
|
||||||
pythonProject
|
pythonProject
|
||||||
} from '../../../../utils/challenge-types';
|
} from '../../../../utils/challenge-types';
|
||||||
import { Form } from '../../../components/formHelpers';
|
import { Form, ValidatedValues } from '../../../components/formHelpers';
|
||||||
|
|
||||||
interface SubmitProps {
|
interface SubmitProps {
|
||||||
showCompletionModal: boolean;
|
showCompletionModal: boolean;
|
||||||
@ -21,12 +21,6 @@ interface FormProps extends WithTranslation {
|
|||||||
updateSolutionForm: (arg0: Record<string, unknown>) => void;
|
updateSolutionForm: (arg0: Record<string, unknown>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ValidatedValues {
|
|
||||||
errors: string[];
|
|
||||||
invalidValues: string[];
|
|
||||||
values: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SolutionForm extends Component<FormProps> {
|
export class SolutionForm extends Component<FormProps> {
|
||||||
constructor(props: FormProps) {
|
constructor(props: FormProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Grid, ListGroup, ListGroupItem } from '@freecodecamp/react-bootstrap';
|
import { Grid, ListGroup, ListGroupItem } from '@freecodecamp/react-bootstrap';
|
||||||
import { Link, graphql } from 'gatsby';
|
import { Link, graphql } from 'gatsby';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -8,18 +7,19 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import ButtonSpacer from '../../components/helpers/button-spacer';
|
import ButtonSpacer from '../../components/helpers/button-spacer';
|
||||||
import FullWidthRow from '../../components/helpers/full-width-row';
|
import FullWidthRow from '../../components/helpers/full-width-row';
|
||||||
import LearnLayout from '../../components/layouts/learn';
|
import LearnLayout from '../../components/layouts/learn';
|
||||||
import { MarkdownRemark, AllChallengeNode } from '../../redux/prop-types';
|
import {
|
||||||
|
MarkdownRemarkType,
|
||||||
|
AllChallengeNodeType,
|
||||||
|
ChallengeNodeType
|
||||||
|
} from '../../redux/prop-types';
|
||||||
|
|
||||||
import './intro.css';
|
import './intro.css';
|
||||||
|
|
||||||
const propTypes = {
|
function renderMenuItems({
|
||||||
data: PropTypes.shape({
|
edges = []
|
||||||
markdownRemark: MarkdownRemark,
|
}: {
|
||||||
allChallengeNode: AllChallengeNode
|
edges?: Array<{ node: ChallengeNodeType }>;
|
||||||
})
|
}) {
|
||||||
};
|
|
||||||
|
|
||||||
function renderMenuItems({ edges = [] }) {
|
|
||||||
return edges
|
return edges
|
||||||
.map(({ node }) => node)
|
.map(({ node }) => node)
|
||||||
.map(({ title, fields: { slug } }) => (
|
.map(({ title, fields: { slug } }) => (
|
||||||
@ -29,7 +29,14 @@ function renderMenuItems({ edges = [] }) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
function IntroductionPage({ data: { markdownRemark, allChallengeNode } }) {
|
function IntroductionPage({
|
||||||
|
data: { markdownRemark, allChallengeNode }
|
||||||
|
}: {
|
||||||
|
data: {
|
||||||
|
markdownRemark: MarkdownRemarkType;
|
||||||
|
allChallengeNode: AllChallengeNodeType;
|
||||||
|
};
|
||||||
|
}): React.FunctionComponentElement<typeof LearnLayout> {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
html,
|
html,
|
||||||
@ -78,7 +85,6 @@ function IntroductionPage({ data: { markdownRemark, allChallengeNode } }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IntroductionPage.displayName = 'IntroductionPage';
|
IntroductionPage.displayName = 'IntroductionPage';
|
||||||
IntroductionPage.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default IntroductionPage;
|
export default IntroductionPage;
|
||||||
|
|
@ -1,16 +1,16 @@
|
|||||||
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
|
import { Grid, Row, Col } from '@freecodecamp/react-bootstrap';
|
||||||
|
import { WindowLocation } from '@reach/router';
|
||||||
import { graphql } from 'gatsby';
|
import { graphql } from 'gatsby';
|
||||||
import { uniq } from 'lodash-es';
|
import { uniq } from 'lodash-es';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Fragment, useEffect, memo } from 'react';
|
import React, { Fragment, useEffect, memo } from 'react';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { TFunction, withTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { configureAnchors } from 'react-scrollable-anchor';
|
import { configureAnchors } from 'react-scrollable-anchor';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import DonateModal from '../../../../client/src/components/Donation/DonationModal';
|
import DonateModal from '../../components/Donation/DonationModal';
|
||||||
import Login from '../../components/Header/components/Login';
|
import Login from '../../components/Header/components/Login';
|
||||||
import Map from '../../components/Map';
|
import Map from '../../components/Map';
|
||||||
import { Spacer } from '../../components/helpers';
|
import { Spacer } from '../../components/helpers';
|
||||||
@ -22,7 +22,11 @@ import {
|
|||||||
tryToShowDonationModal,
|
tryToShowDonationModal,
|
||||||
userSelector
|
userSelector
|
||||||
} from '../../redux';
|
} from '../../redux';
|
||||||
import { MarkdownRemark, AllChallengeNode, User } from '../../redux/prop-types';
|
import {
|
||||||
|
MarkdownRemarkType,
|
||||||
|
AllChallengeNodeType,
|
||||||
|
UserType
|
||||||
|
} from '../../redux/prop-types';
|
||||||
import Block from './components/Block';
|
import Block from './components/Block';
|
||||||
import CertChallenge from './components/CertChallenge';
|
import CertChallenge from './components/CertChallenge';
|
||||||
import SuperBlockIntro from './components/SuperBlockIntro';
|
import SuperBlockIntro from './components/SuperBlockIntro';
|
||||||
@ -30,44 +34,48 @@ import { resetExpansion, toggleBlock } from './redux';
|
|||||||
|
|
||||||
import './intro.css';
|
import './intro.css';
|
||||||
|
|
||||||
const propTypes = {
|
type FetchState = {
|
||||||
currentChallengeId: PropTypes.string,
|
pending: boolean;
|
||||||
data: PropTypes.shape({
|
complete: boolean;
|
||||||
markdownRemark: MarkdownRemark,
|
errored: boolean;
|
||||||
allChallengeNode: AllChallengeNode
|
};
|
||||||
}),
|
|
||||||
expandedState: PropTypes.object,
|
type SuperBlockProp = {
|
||||||
fetchState: PropTypes.shape({
|
currentChallengeId: string;
|
||||||
pending: PropTypes.bool,
|
data: {
|
||||||
complete: PropTypes.bool,
|
markdownRemark: MarkdownRemarkType;
|
||||||
errored: PropTypes.bool
|
allChallengeNode: AllChallengeNodeType;
|
||||||
}),
|
};
|
||||||
isSignedIn: PropTypes.bool,
|
expandedState: {
|
||||||
location: PropTypes.shape({
|
[key: string]: boolean;
|
||||||
hash: PropTypes.string,
|
};
|
||||||
// TODO: state is sometimes a string
|
fetchState: FetchState;
|
||||||
state: PropTypes.shape({
|
isSignedIn: boolean;
|
||||||
breadcrumbBlockClick: PropTypes.string
|
signInLoading: boolean;
|
||||||
})
|
location: WindowLocation<{ breadcrumbBlockClick: string }>;
|
||||||
}),
|
resetExpansion: () => void;
|
||||||
resetExpansion: PropTypes.func,
|
t: TFunction;
|
||||||
signInLoading: PropTypes.bool,
|
toggleBlock: (arg0: string) => void;
|
||||||
t: PropTypes.func,
|
tryToShowDonationModal: () => void;
|
||||||
toggleBlock: PropTypes.func,
|
user: UserType;
|
||||||
tryToShowDonationModal: PropTypes.func.isRequired,
|
|
||||||
user: User
|
|
||||||
};
|
};
|
||||||
|
|
||||||
configureAnchors({ offset: -40, scrollDuration: 0 });
|
configureAnchors({ offset: -40, scrollDuration: 0 });
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = (state: unknown) => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
currentChallengeIdSelector,
|
currentChallengeIdSelector,
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
(currentChallengeId, isSignedIn, signInLoading, fetchState, user) => ({
|
(
|
||||||
|
currentChallengeId: string,
|
||||||
|
isSignedIn,
|
||||||
|
signInLoading: boolean,
|
||||||
|
fetchState: FetchState,
|
||||||
|
user: UserType
|
||||||
|
) => ({
|
||||||
currentChallengeId,
|
currentChallengeId,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
signInLoading,
|
signInLoading,
|
||||||
@ -77,7 +85,7 @@ const mapStateToProps = state => {
|
|||||||
)(state);
|
)(state);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
{
|
{
|
||||||
tryToShowDonationModal,
|
tryToShowDonationModal,
|
||||||
@ -87,7 +95,7 @@ const mapDispatchToProps = dispatch =>
|
|||||||
dispatch
|
dispatch
|
||||||
);
|
);
|
||||||
|
|
||||||
const SuperBlockIntroductionPage = props => {
|
const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initializeExpandedState();
|
initializeExpandedState();
|
||||||
props.tryToShowDonationModal();
|
props.tryToShowDonationModal();
|
||||||
@ -102,7 +110,7 @@ const SuperBlockIntroductionPage = props => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getChosenBlock = () => {
|
const getChosenBlock = (): string => {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
allChallengeNode: { edges }
|
allChallengeNode: { edges }
|
||||||
@ -110,10 +118,14 @@ const SuperBlockIntroductionPage = props => {
|
|||||||
isSignedIn,
|
isSignedIn,
|
||||||
currentChallengeId,
|
currentChallengeId,
|
||||||
location
|
location
|
||||||
} = props;
|
}: SuperBlockProp = props;
|
||||||
|
|
||||||
// if coming from breadcrumb click
|
// if coming from breadcrumb click
|
||||||
if (location.state && location.state.breadcrumbBlockClick) {
|
if (
|
||||||
|
location.state &&
|
||||||
|
typeof location.state === 'object' &&
|
||||||
|
location.state.hasOwnProperty('breadcrumbBlockClick')
|
||||||
|
) {
|
||||||
return location.state.breadcrumbBlockClick;
|
return location.state.breadcrumbBlockClick;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +135,7 @@ const SuperBlockIntroductionPage = props => {
|
|||||||
return dashedBlock;
|
return dashedBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
let edge = edges[0];
|
const edge = edges[0];
|
||||||
|
|
||||||
if (isSignedIn) {
|
if (isSignedIn) {
|
||||||
// see if currentChallenge is in this superBlock
|
// see if currentChallenge is in this superBlock
|
||||||
@ -232,7 +244,6 @@ const SuperBlockIntroductionPage = props => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SuperBlockIntroductionPage.displayName = 'SuperBlockIntroductionPage';
|
SuperBlockIntroductionPage.displayName = 'SuperBlockIntroductionPage';
|
||||||
SuperBlockIntroductionPage.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
@ -21,11 +21,11 @@ const codeally = path.resolve(
|
|||||||
);
|
);
|
||||||
const intro = path.resolve(
|
const intro = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../../src/templates/Introduction/Intro.js'
|
'../../src/templates/Introduction/intro.tsx'
|
||||||
);
|
);
|
||||||
const superBlockIntro = path.resolve(
|
const superBlockIntro = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
'../../src/templates/Introduction/SuperBlockIntro.js'
|
'../../src/templates/Introduction/super-block-intro.tsx'
|
||||||
);
|
);
|
||||||
const video = path.resolve(
|
const video = path.resolve(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
Reference in New Issue
Block a user