mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-10-24 12:13:31 +08:00
feat: migrate setting modals (#54112)
Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Co-authored-by: ahmad abdolsaheb <ahmad.abdolsaheb@gmail.com>
This commit is contained in:
co-authored by
Huyen Nguyen
ahmad abdolsaheb
parent
f6ae52f6fe
commit
76b4c812cb
@@ -1,8 +1,7 @@
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Row, Button } from '@freecodecamp/ui';
|
||||
import { Row, Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
import type { GeneratedExamResults } from '../../redux/prop-types';
|
||||
import { closeModal } from '../../templates/Challenges/redux/actions';
|
||||
@@ -51,36 +50,36 @@ const ExamResultsModal = ({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
aria-labelledby='solution-viewer-modal-title'
|
||||
bsSize='large'
|
||||
onHide={() => {
|
||||
onClose={() => {
|
||||
closeModal('examResults');
|
||||
}}
|
||||
show={isOpen}
|
||||
size='lg'
|
||||
open={isOpen}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title id='solution-viewer-modal-title'>
|
||||
{t('settings.labels.results-for', { projectTitle })}
|
||||
</Modal.Title>
|
||||
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||
{t('settings.labels.results-for', { projectTitle })}
|
||||
</Modal.Header>
|
||||
<Modal.Body style={{ paddingLeft: 30 }}>
|
||||
<Modal.Body>
|
||||
<Spacer size='medium' />
|
||||
<Row>
|
||||
{t('learn.exam.number-of-questions', {
|
||||
n: numberOfQuestionsInExam
|
||||
})}
|
||||
</Row>{' '}
|
||||
<Spacer size='medium' />
|
||||
<Row>
|
||||
{t('learn.exam.correct-answers', { n: numberOfCorrectAnswers })}
|
||||
</Row>{' '}
|
||||
<Spacer size='medium' />
|
||||
<Row>{t('learn.exam.percent-correct', { n: percentCorrect })}</Row>
|
||||
<Spacer size='medium' />{' '}
|
||||
<Row>
|
||||
{t('learn.exam.time', { t: formatSecondsToTime(examTimeInSeconds) })}
|
||||
</Row>
|
||||
<div style={{ paddingLeft: '30px' }}>
|
||||
<Row>
|
||||
{t('learn.exam.number-of-questions', {
|
||||
n: numberOfQuestionsInExam
|
||||
})}
|
||||
</Row>{' '}
|
||||
<Spacer size='medium' />
|
||||
<Row>
|
||||
{t('learn.exam.correct-answers', { n: numberOfCorrectAnswers })}
|
||||
</Row>{' '}
|
||||
<Spacer size='medium' />
|
||||
<Row>{t('learn.exam.percent-correct', { n: percentCorrect })}</Row>
|
||||
<Spacer size='medium' />{' '}
|
||||
<Row>
|
||||
{t('learn.exam.time', {
|
||||
t: formatSecondsToTime(examTimeInSeconds)
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
<Spacer size='medium' />
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
import { Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
import { CompletedChallenge } from '../../redux/prop-types';
|
||||
import SolutionViewer from './solution-viewer';
|
||||
@@ -23,23 +22,14 @@ const ProjectModal = ({
|
||||
}: ProjectModalProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal
|
||||
data-playwright-test-label='project-solution-viewer-modal'
|
||||
aria-labelledby='solution-viewer-modal-title'
|
||||
bsSize='large'
|
||||
onHide={handleSolutionModalHide}
|
||||
show={isOpen}
|
||||
size='lg'
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title id='solution-viewer-modal-title'>
|
||||
{t('settings.labels.solution-for', { projectTitle })}
|
||||
</Modal.Title>
|
||||
<Modal onClose={handleSolutionModalHide} open={isOpen} size='large'>
|
||||
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||
{t('settings.labels.solution-for', { projectTitle })}
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Modal.Body alignment='left'>
|
||||
<SolutionViewer challengeFiles={challengeFiles} solution={solution} />
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Modal.Footer alignment='end'>
|
||||
<Button
|
||||
data-cy='solution-viewer-close-btn'
|
||||
onClick={handleSolutionModalHide}
|
||||
|
||||
@@ -576,3 +576,14 @@ blockquote .small {
|
||||
color: white;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
/*
|
||||
* /learn sets some default styles to all `h2`s.
|
||||
* These rules are to override the defaults and apply danger styles to `danger` modal.
|
||||
*/
|
||||
#headlessui-portal-root h2 {
|
||||
font-weight: normal;
|
||||
}
|
||||
#headlessui-portal-root .text-background-danger h2 {
|
||||
color: var(--background-danger);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ import React, { useMemo, useState } from 'react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import { Table, Button } from '@freecodecamp/ui';
|
||||
import { Table, Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
import envData from '../../../../config/env.json';
|
||||
import { getLangCode } from '../../../../../shared/config/i18n';
|
||||
@@ -197,17 +196,11 @@ function TimelineInner({
|
||||
</Table>
|
||||
)}
|
||||
{id && (
|
||||
<Modal
|
||||
aria-labelledby='contained-modal-title'
|
||||
onHide={closeSolution}
|
||||
show={solutionOpen}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title id='contained-modal-title' className='text-center'>
|
||||
{`${username}'s Solution to ${
|
||||
idToNameMap.get(id)?.challengeTitle ?? ''
|
||||
}`}
|
||||
</Modal.Title>
|
||||
<Modal onClose={closeSolution} open={solutionOpen}>
|
||||
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||
{`${username}'s Solution to ${
|
||||
idToNameMap.get(id)?.challengeTitle ?? ''
|
||||
}`}
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<SolutionViewer
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import React from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
import { Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
import { Spacer } from '../helpers';
|
||||
|
||||
@@ -16,19 +15,9 @@ function DeleteModal(props: DeleteModalProps): JSX.Element {
|
||||
const email = 'support@freecodecamp.org';
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal
|
||||
aria-labelledby='modal-title'
|
||||
backdrop={true}
|
||||
bsSize='lg'
|
||||
className='text-center'
|
||||
keyboard={true}
|
||||
onHide={onHide}
|
||||
show={show}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title id='modal-title'>
|
||||
{t('settings.danger.delete-title')}
|
||||
</Modal.Title>
|
||||
<Modal onClose={onHide} open={show} variant='danger' size='large'>
|
||||
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||
{t('settings.danger.delete-title')}
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>{t('settings.danger.delete-p1')}</p>
|
||||
@@ -40,7 +29,8 @@ function DeleteModal(props: DeleteModalProps): JSX.Element {
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
<hr />
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
block={true}
|
||||
size='large'
|
||||
@@ -60,9 +50,6 @@ function DeleteModal(props: DeleteModalProps): JSX.Element {
|
||||
>
|
||||
{t('settings.danger.certain')}
|
||||
</Button>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={props.onHide}>{t('buttons.close')}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
import { Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
import { Spacer } from '../helpers';
|
||||
|
||||
@@ -16,24 +15,15 @@ function ResetModal(props: ResetModalProps): JSX.Element {
|
||||
const { show, onHide } = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
aria-labelledby='modal-title'
|
||||
backdrop={true}
|
||||
bsSize='lg'
|
||||
className='text-center'
|
||||
keyboard={true}
|
||||
onHide={onHide}
|
||||
show={show}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title id='modal-title'>
|
||||
{t('settings.danger.reset-heading')}
|
||||
</Modal.Title>
|
||||
<Modal size='large' onClose={onHide} variant='danger' open={show}>
|
||||
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||
{t('settings.danger.reset-heading')}
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>{t('settings.danger.reset-p1')}</p>
|
||||
<p>{t('settings.danger.reset-p2')}</p>
|
||||
<hr />
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
block={true}
|
||||
size='large'
|
||||
@@ -53,9 +43,6 @@ function ResetModal(props: ResetModalProps): JSX.Element {
|
||||
>
|
||||
{t('settings.danger.reset-confirm')}
|
||||
</Button>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={props.onHide}>{t('buttons.close')}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { connect } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
import { Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
import { Spacer } from '../helpers';
|
||||
import { hardGoTo as navigate, closeSignoutModal } from '../../redux/actions';
|
||||
@@ -47,20 +46,9 @@ function SignoutModal(props: SignoutModalProps): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
aria-labelledby='modal-title'
|
||||
backdrop={true}
|
||||
bsSize='lg'
|
||||
className='text-center'
|
||||
keyboard={true}
|
||||
onHide={handleModalHide}
|
||||
onClose={handleModalHide}
|
||||
show={show}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title id='modal-title' bsSize='lg'>
|
||||
<span style={{ fontWeight: 'bold' }}>{t('signout.heading')}</span>
|
||||
</Modal.Title>
|
||||
<Modal size='large' variant='danger' open={show} onClose={handleModalHide}>
|
||||
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||
{t('signout.heading')}
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
import { Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
import store from 'store';
|
||||
import { Spacer } from '../helpers';
|
||||
@@ -17,23 +16,11 @@ function StagingWarningModal(): JSX.Element {
|
||||
setShow(false);
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
aria-labelledby='modal-title'
|
||||
backdrop={true}
|
||||
bsSize='lg'
|
||||
className='text-center'
|
||||
keyboard={true}
|
||||
onHide={handleModalHide}
|
||||
onClose={handleModalHide}
|
||||
show={show}
|
||||
data-testid={'staging-warning-modal'}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title id='modal-title' bsSize='lg'>
|
||||
<span style={{ fontWeight: 'bold' }}>
|
||||
{t('staging-warning.heading')}
|
||||
</span>
|
||||
</Modal.Title>
|
||||
<Modal onClose={handleModalHide} open={show} size='large'>
|
||||
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||
<span style={{ fontWeight: 'bold' }}>
|
||||
{t('staging-warning.heading')}
|
||||
</span>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p className='text-justify'>{t('staging-warning.p1')}</p>
|
||||
|
||||
@@ -2,23 +2,40 @@ import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import store from 'store';
|
||||
import StagingWarningModal from '.';
|
||||
|
||||
describe('StagingWarningModal', () => {
|
||||
beforeAll(() => {
|
||||
// The Modal component uses `ResizeObserver` under the hood.
|
||||
// However, this property is not available in JSDOM, so we need to manually add it to the window object.
|
||||
// Ref: https://github.com/jsdom/jsdom/issues/3368
|
||||
Object.defineProperty(window, 'ResizeObserver', {
|
||||
writable: true,
|
||||
value: jest.fn(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn()
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the modal successfully', () => {
|
||||
render(<StagingWarningModal />);
|
||||
expect(screen.getByTestId('staging-warning-modal')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('staging-warning-modal')).toHaveClass('in');
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('closes the modal when clicking the close button', () => {
|
||||
render(<StagingWarningModal />);
|
||||
fireEvent.click(screen.getByText('Close'));
|
||||
expect(screen.getByTestId('staging-warning-modal')).not.toHaveClass('in');
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays the correct modal content', () => {
|
||||
render(<StagingWarningModal />);
|
||||
const modalContent = screen.getByTestId('staging-warning-modal');
|
||||
const modalContent = screen.getByRole('dialog');
|
||||
expect(modalContent).toHaveTextContent('staging-warning.heading');
|
||||
expect(modalContent).toHaveTextContent('staging-warning.p1');
|
||||
expect(modalContent).toHaveTextContent('staging-warning.p2');
|
||||
@@ -27,8 +44,10 @@ describe('StagingWarningModal', () => {
|
||||
|
||||
it('accepts Warning, stores acceptance key in local storage, and closes the modal', () => {
|
||||
render(<StagingWarningModal />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('accepts-warning'));
|
||||
expect(store.get('acceptedStagingWarning')).toBe(true);
|
||||
expect(screen.queryByTestId('staging-warning-modal')).not.toHaveClass('in');
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
.reset-modal p {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.reset-modal-header {
|
||||
color: var(--danger-color);
|
||||
background-color: var(--danger-background);
|
||||
}
|
||||
|
||||
.reset-modal-header h4 {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.reset-modal-header .close {
|
||||
color: var(--danger-color);
|
||||
font-size: 28px;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.reset-modal .btn-lg {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,20 @@
|
||||
// Package Utilities
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
import { Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
// Local Utilities
|
||||
import { closeModal, resetChallenge } from '../redux/actions';
|
||||
import { isResetModalOpenSelector } from '../redux/selectors';
|
||||
import callGA from '../../../analytics/call-ga';
|
||||
|
||||
// Styles
|
||||
import './reset-modal.css';
|
||||
|
||||
// Types
|
||||
interface ResetModalProps {
|
||||
close: () => void;
|
||||
isOpen: boolean;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// Redux Setup
|
||||
const mapStateToProps = createSelector(
|
||||
isResetModalOpenSelector,
|
||||
(isOpen: boolean) => ({
|
||||
@@ -43,25 +35,17 @@ function withActions(...fns: Array<() => void>) {
|
||||
return () => fns.forEach(fn => fn());
|
||||
}
|
||||
|
||||
// Component
|
||||
function ResetModal({ reset, close, isOpen }: ResetModalProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
if (isOpen) {
|
||||
callGA({ event: 'pageview', pagePath: '/reset-modal' });
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
data-playwright-test-label='reset-modal'
|
||||
animation={false}
|
||||
dialogClassName='reset-modal'
|
||||
keyboard={true}
|
||||
onHide={close}
|
||||
show={isOpen}
|
||||
>
|
||||
<Modal.Header className='reset-modal-header' closeButton={true}>
|
||||
<Modal.Title className='text-center'>{t('learn.reset')}</Modal.Title>
|
||||
<Modal onClose={close} open={isOpen} variant='danger'>
|
||||
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||
{t('learn.reset')}
|
||||
</Modal.Header>
|
||||
<Modal.Body className='reset-modal-body'>
|
||||
<Modal.Body>
|
||||
<div className='text-center'>
|
||||
<p>{t('learn.reset-warn')}</p>
|
||||
<p>
|
||||
@@ -69,7 +53,7 @@ function ResetModal({ reset, close, isOpen }: ResetModalProps): JSX.Element {
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className='reset-modal-footer'>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
data-cy='reset-modal-confirm'
|
||||
block={true}
|
||||
|
||||
Reference in New Issue
Block a user