mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client): show loading icon when preview frame has not loaded yet (#66687)
This commit is contained in:
@@ -1,6 +1,22 @@
|
||||
.project-preview-modal-body {
|
||||
position: relative;
|
||||
line-height: 0;
|
||||
padding: 0;
|
||||
min-height: 70vh;
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.project-preview-modal-loader {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background-color: var(--primary-background);
|
||||
}
|
||||
|
||||
.project-preview-modal-content {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.project-preview-modal-content.is-loading {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
import { Loader } from '../../../components/helpers';
|
||||
import type { ChallengeData } from '../../../redux/prop-types';
|
||||
import {
|
||||
closeModal,
|
||||
setEditorFocusability,
|
||||
projectPreviewMounted
|
||||
} from '../redux/actions';
|
||||
import { isProjectPreviewModalOpenSelector } from '../redux/selectors';
|
||||
import {
|
||||
isProjectPreviewLoadingSelector,
|
||||
isProjectPreviewModalOpenSelector
|
||||
} from '../redux/selectors';
|
||||
import { projectPreviewId } from '../utils/frame';
|
||||
import Preview from './preview';
|
||||
|
||||
@@ -21,6 +25,7 @@ interface ProjectPreviewMountedPayload {
|
||||
interface Props {
|
||||
closeModal: (arg: string) => void;
|
||||
isOpen: boolean;
|
||||
isLoading: boolean;
|
||||
projectPreviewMounted: (payload: ProjectPreviewMountedPayload) => void;
|
||||
challengeData?: ChallengeData | null;
|
||||
setEditorFocusability: (focusability: boolean) => void;
|
||||
@@ -29,7 +34,8 @@ interface Props {
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: unknown) => ({
|
||||
isOpen: isProjectPreviewModalOpenSelector(state) as boolean
|
||||
isOpen: isProjectPreviewModalOpenSelector(state) as boolean,
|
||||
isLoading: isProjectPreviewLoadingSelector(state) as boolean
|
||||
});
|
||||
const mapDispatchToProps = {
|
||||
closeModal,
|
||||
@@ -40,6 +46,7 @@ const mapDispatchToProps = {
|
||||
function ProjectPreviewModal({
|
||||
closeModal,
|
||||
isOpen,
|
||||
isLoading,
|
||||
projectPreviewMounted,
|
||||
challengeData = null,
|
||||
setEditorFocusability,
|
||||
@@ -48,7 +55,11 @@ function ProjectPreviewModal({
|
||||
}: Props): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (isOpen) setEditorFocusability(false);
|
||||
});
|
||||
}, [isOpen, setEditorFocusability]);
|
||||
|
||||
const handlePreviewMounted = useCallback(() => {
|
||||
projectPreviewMounted({ challengeData });
|
||||
}, [projectPreviewMounted, challengeData]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -61,10 +72,21 @@ function ProjectPreviewModal({
|
||||
>
|
||||
<Modal.Header closeButtonClassNames='close'>{previewTitle}</Modal.Header>
|
||||
<Modal.Body className='project-preview-modal-body'>
|
||||
<Preview
|
||||
previewId={projectPreviewId}
|
||||
previewMounted={() => projectPreviewMounted({ challengeData })}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<div className='project-preview-modal-loader'>
|
||||
<Loader />
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={`project-preview-modal-content ${
|
||||
isLoading ? 'is-loading' : ''
|
||||
}`}
|
||||
>
|
||||
<Preview
|
||||
previewId={projectPreviewId}
|
||||
previewMounted={handlePreviewMounted}
|
||||
/>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
|
||||
@@ -34,6 +34,7 @@ export const actionTypes = createTypes(
|
||||
'setUserCompletedExam',
|
||||
'previewMounted',
|
||||
'projectPreviewMounted',
|
||||
'setProjectPreviewLoading',
|
||||
'storePortalWindow',
|
||||
'removePortalWindow',
|
||||
'challengeMounted',
|
||||
|
||||
@@ -49,6 +49,9 @@ export const previewMounted = createAction(actionTypes.previewMounted);
|
||||
export const projectPreviewMounted = createAction(
|
||||
actionTypes.projectPreviewMounted
|
||||
);
|
||||
export const setProjectPreviewLoading = createAction(
|
||||
actionTypes.setProjectPreviewLoading
|
||||
);
|
||||
|
||||
export const storePortalWindow = createAction(actionTypes.storePortalWindow);
|
||||
export const removePortalWindow = createAction(actionTypes.removePortalWindow);
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
initLogs,
|
||||
logsToConsole,
|
||||
openModal,
|
||||
setProjectPreviewLoading,
|
||||
updateConsole,
|
||||
updateLogs,
|
||||
updateTests
|
||||
@@ -369,10 +370,11 @@ function* updatePython(challengeData) {
|
||||
}
|
||||
|
||||
function* previewProjectSolutionSaga({ payload }) {
|
||||
if (!payload?.challengeData) return;
|
||||
const { challengeData } = payload;
|
||||
|
||||
yield put(setProjectPreviewLoading(true));
|
||||
try {
|
||||
if (!payload?.challengeData) return;
|
||||
const { challengeData } = payload;
|
||||
|
||||
if (canBuildChallenge(challengeData)) {
|
||||
const buildData = yield buildChallengeData(challengeData);
|
||||
if (buildData.error) throw Error(buildData.error);
|
||||
@@ -387,6 +389,8 @@ function* previewProjectSolutionSaga({ payload }) {
|
||||
} catch (err) {
|
||||
console.error('Unable to show project preview');
|
||||
console.error(err);
|
||||
} finally {
|
||||
yield put(setProjectPreviewLoading(false));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ const initialState = {
|
||||
portalWindow: null,
|
||||
showPreviewPortal: false,
|
||||
showPreviewPane: true,
|
||||
isProjectPreviewLoading: false,
|
||||
projectFormValues: {},
|
||||
successMessage: 'Happy Coding!',
|
||||
isAdvancing: false,
|
||||
@@ -270,12 +271,23 @@ export const reducer = handleActions(
|
||||
[payload]: false
|
||||
}
|
||||
}),
|
||||
[actionTypes.openModal]: (state, { payload }) => ({
|
||||
[actionTypes.openModal]: (state, { payload }) => {
|
||||
const isProjectPreviewModal = payload === 'projectPreview';
|
||||
|
||||
return {
|
||||
...state,
|
||||
modal: {
|
||||
...state.modal,
|
||||
[payload]: true
|
||||
},
|
||||
isProjectPreviewLoading: isProjectPreviewModal
|
||||
? true
|
||||
: state.isProjectPreviewLoading
|
||||
};
|
||||
},
|
||||
[actionTypes.setProjectPreviewLoading]: (state, { payload }) => ({
|
||||
...state,
|
||||
modal: {
|
||||
...state.modal,
|
||||
[payload]: true
|
||||
}
|
||||
isProjectPreviewLoading: payload
|
||||
}),
|
||||
[actionTypes.executeChallenge]: state => ({
|
||||
...state,
|
||||
|
||||
@@ -52,6 +52,8 @@ export const isFinishQuizModalOpenSelector = state =>
|
||||
state[ns].modal.finishQuiz;
|
||||
export const isProjectPreviewModalOpenSelector = state =>
|
||||
state[ns].modal.projectPreview;
|
||||
export const isProjectPreviewLoadingSelector = state =>
|
||||
state[ns].isProjectPreviewLoading;
|
||||
export const isShortcutsModalOpenSelector = state => state[ns].modal.shortcuts;
|
||||
export const isSpeakingModalOpenSelector = state => state[ns].modal.speaking;
|
||||
export const isSubmittingSelector = state => state[ns].isSubmitting;
|
||||
|
||||
@@ -71,12 +71,15 @@ function getDocumentTitle(buildData: BuildChallengeData) {
|
||||
export function updateProjectPreview(
|
||||
buildData: BuildChallengeData,
|
||||
document: Document
|
||||
): void {
|
||||
): Promise<void> {
|
||||
if (challengeHasPreview(buildData)) {
|
||||
createProjectPreviewFramer(
|
||||
document,
|
||||
getDocumentTitle(buildData)
|
||||
)(buildData);
|
||||
return new Promise<void>(resolve =>
|
||||
createProjectPreviewFramer(
|
||||
document,
|
||||
getDocumentTitle(buildData),
|
||||
resolve
|
||||
)(buildData)
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Cannot show preview for challenge type ${buildData.challengeType}`
|
||||
|
||||
@@ -393,7 +393,16 @@ function handleDocumentNotFound(err: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const initPreviewFrame = () => (frameContext: Context) => frameContext;
|
||||
const initProjectPreviewFrame =
|
||||
(frameReady?: () => void) => (frameContext: Context) => {
|
||||
waitForFrame(frameContext)
|
||||
.then(() => {
|
||||
if (frameReady) frameReady();
|
||||
})
|
||||
.catch(handleDocumentNotFound);
|
||||
|
||||
return frameContext;
|
||||
};
|
||||
|
||||
const waitForFrame = (frameContext: Context) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
@@ -445,12 +454,14 @@ export const createMainPreviewFramer = (
|
||||
|
||||
export const createProjectPreviewFramer = (
|
||||
document: Document,
|
||||
frameTitle: string
|
||||
frameTitle: string,
|
||||
frameReady?: () => void
|
||||
): ((args: Context) => void) =>
|
||||
createFramer({
|
||||
document,
|
||||
id: projectPreviewId,
|
||||
init: initPreviewFrame,
|
||||
init: initProjectPreviewFrame,
|
||||
frameReady,
|
||||
frameTitle
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user