feat(client): show loading icon when preview frame has not loaded yet (#66687)

This commit is contained in:
Sem Bauke
2026-04-08 09:59:27 +02:00
committed by GitHub
parent 3cb26fc6a3
commit ddac8f0593
9 changed files with 98 additions and 24 deletions
@@ -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));
}
}
+17 -5
View File
@@ -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}`
+14 -3
View File
@@ -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
});