From ddac8f0593bee54752e61194bafe42107f8a90b1 Mon Sep 17 00:00:00 2001 From: Sem Bauke Date: Wed, 8 Apr 2026 09:59:27 +0200 Subject: [PATCH] feat(client): show loading icon when preview frame has not loaded yet (#66687) --- .../components/project-preview-modal.css | 16 ++++++++ .../components/project-preview-modal.tsx | 38 +++++++++++++++---- .../Challenges/redux/action-types.js | 1 + .../src/templates/Challenges/redux/actions.js | 3 ++ .../redux/execute-challenge-saga.js | 10 +++-- .../src/templates/Challenges/redux/index.js | 22 ++++++++--- .../templates/Challenges/redux/selectors.js | 2 + .../src/templates/Challenges/utils/build.ts | 13 ++++--- .../src/templates/Challenges/utils/frame.ts | 17 +++++++-- 9 files changed, 98 insertions(+), 24 deletions(-) diff --git a/client/src/templates/Challenges/components/project-preview-modal.css b/client/src/templates/Challenges/components/project-preview-modal.css index 4b436f27467..c54a516b060 100644 --- a/client/src/templates/Challenges/components/project-preview-modal.css +++ b/client/src/templates/Challenges/components/project-preview-modal.css @@ -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; +} diff --git a/client/src/templates/Challenges/components/project-preview-modal.tsx b/client/src/templates/Challenges/components/project-preview-modal.tsx index 23d1caa1a42..dc93a3c52bd 100644 --- a/client/src/templates/Challenges/components/project-preview-modal.tsx +++ b/client/src/templates/Challenges/components/project-preview-modal.tsx @@ -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 ( {previewTitle} - projectPreviewMounted({ challengeData })} - /> + {isLoading ? ( +
+ +
+ ) : null} +
+ +