Skip to content

Commit 538d3b9

Browse files
Suvam-paul145priyankarpalCopilot
authored
Route-Based Code Splitting (#1692)
* fix: sanitize dangerouslySetInnerHTML with DOMPurify to prevent XSS * feat: introduce several new plays, common UI components, and a sanitizeHTML utility. * Eslint version issues solved * Implemented React.lazy() + dynamic import() for all 114 plays, enabling Webpack to produce per-play code-split chunks. Users now only download a play's code when they navigate to it. * unusual files are reverted from this branch * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Priyankar Pal <88102392+priyankarpal@users.noreply.github.com> * fix: restore sanitizeHTML for XSS protection and fix Tube2tunes loading state * retaining code structure of CSS file --------- Signed-off-by: Priyankar Pal <88102392+priyankarpal@users.noreply.github.com> Co-authored-by: Priyankar Pal <88102392+priyankarpal@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 0566091 commit 538d3b9

11 files changed

Lines changed: 146 additions & 11 deletions

File tree

src/common/badges-dashboard/BadgeDetails.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Badge from './Badge';
2-
import sanitizeHTML from 'common/utils/sanitizeHTML';
32
import './badge.css';
3+
import sanitizeHTML from 'common/utils/sanitizeHTML';
44

55
const BadgeDetails = ({ badge, onClose }) => {
66
const makeClickableLinks = (badge) => {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
.play-error-container {
2+
display: flex;
3+
flex-direction: column;
4+
align-items: center;
5+
justify-content: center;
6+
padding: 3rem 1.5rem;
7+
text-align: center;
8+
min-height: 50vh;
9+
}
10+
11+
.play-error-image {
12+
width: 200px;
13+
height: auto;
14+
margin-bottom: 1.5rem;
15+
opacity: 0.8;
16+
}
17+
18+
.play-error-title {
19+
font-size: 1.5rem;
20+
font-weight: 600;
21+
color: #333;
22+
margin: 0 0 0.75rem;
23+
}
24+
25+
.play-error-message {
26+
font-size: 1rem;
27+
color: #666;
28+
max-width: 500px;
29+
line-height: 1.5;
30+
margin: 0 0 1.5rem;
31+
}
32+
33+
.play-error-actions {
34+
display: flex;
35+
gap: 1rem;
36+
}
37+
38+
.play-error-retry-button {
39+
padding: 0.6rem 1.5rem;
40+
font-size: 0.95rem;
41+
font-weight: 600;
42+
border: none;
43+
border-radius: 6px;
44+
cursor: pointer;
45+
background: #00f2fe;
46+
color: #fff;
47+
transition: opacity 0.2s;
48+
}
49+
50+
.play-error-back-button {
51+
padding: 0.6rem 1.5rem;
52+
font-size: 0.95rem;
53+
font-weight: 600;
54+
border: 2px solid #00f2fe;
55+
border-radius: 6px;
56+
cursor: pointer;
57+
background: transparent;
58+
color: #00f2fe;
59+
transition: opacity 0.2s;
60+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
import { ReactComponent as ImageOops } from 'images/img-oops.svg';
3+
import './PlayErrorBoundary.css';
4+
5+
class PlayErrorBoundary extends React.Component {
6+
constructor(props) {
7+
super(props);
8+
this.state = { hasError: false, error: null, isChunkError: false };
9+
}
10+
11+
static getDerivedStateFromError(error) {
12+
// Detect chunk load failures (network errors loading lazy chunks)
13+
const isChunkError =
14+
error?.name === 'ChunkLoadError' ||
15+
/loading chunk/i.test(error?.message) ||
16+
/failed to fetch dynamically imported module/i.test(error?.message);
17+
18+
return { hasError: true, error, isChunkError };
19+
}
20+
21+
componentDidCatch(error, errorInfo) {
22+
console.error(`Error loading play "${this.props.playName}":`, error, errorInfo);
23+
}
24+
25+
handleRetry = () => {
26+
this.setState({ hasError: false, error: null, isChunkError: false });
27+
};
28+
29+
handleGoBack = () => {
30+
window.location.href = '/plays';
31+
};
32+
33+
render() {
34+
if (this.state.hasError) {
35+
return (
36+
<div className="play-error-boundary play-error-container">
37+
<ImageOops className="play-error-image" />
38+
<h2 className="play-error-title">
39+
{this.state.isChunkError ? 'Failed to load this play' : 'Something went wrong'}
40+
</h2>
41+
<p className="play-error-message">
42+
{this.state.isChunkError
43+
? 'There was a network error loading this play. Please check your connection and try again.'
44+
: `An error occurred while rendering "${this.props.playName || 'this play'}".`}
45+
</p>
46+
<div className="play-error-actions">
47+
{this.state.isChunkError && (
48+
<button className="play-error-retry-button" onClick={this.handleRetry}>
49+
Retry
50+
</button>
51+
)}
52+
<button className="play-error-back-button" onClick={this.handleGoBack}>
53+
Back to Plays
54+
</button>
55+
</div>
56+
</div>
57+
);
58+
}
59+
60+
return this.props.children;
61+
}
62+
}
63+
64+
export default PlayErrorBoundary;

src/common/playlists/PlayMeta.jsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { PageNotFound } from 'common';
1010
import thumbPlay from 'images/thumb-play.png';
1111
import { getProdUrl } from 'common/utils/commonUtils';
1212
import { loadCoverImage } from 'common/utils/coverImageUtil';
13+
import PlayErrorBoundary from 'common/playlists/PlayErrorBoundary';
1314

1415
function PlayMeta() {
1516
const [loading, setLoading] = useState(true);
@@ -87,6 +88,10 @@ function PlayMeta() {
8788
const renderPlayComponent = () => {
8889
const Comp = plays[play.component || toSanitized(play.title_name)];
8990

91+
if (!Comp) {
92+
return <PageNotFound />;
93+
}
94+
9095
return <Comp {...play} />;
9196
};
9297

@@ -103,7 +108,11 @@ function PlayMeta() {
103108
<meta content={play.description} data-react-helmet="true" name="twitter:description" />
104109
<meta content={ogTagImage} data-react-helmet="true" name="twitter:image" />
105110
</Helmet>
106-
<Suspense fallback={<Loader />}>{renderPlayComponent()}</Suspense>
111+
<Suspense
112+
fallback={<Loader subtitle="Please wait while the play loads" title="Loading Play..." />}
113+
>
114+
<PlayErrorBoundary playName={play.name}>{renderPlayComponent()}</PlayErrorBoundary>
115+
</Suspense>
107116
</>
108117
);
109118
}

src/plays/Selection-Sort-Visualizer/SelectionSortVisualizer.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ function SelectionSortVisualizer() {
2323
const handleSort = async () => {
2424
const arrCopy = [...arr];
2525
const outputElements = document.getElementById('output-visualizer');
26-
// Safe: clears the container to empty string (no user data injected).
27-
// All subsequent DOM mutations use createElement/createTextNode (XSS-safe).
2826
outputElements.innerHTML = '';
2927

3028
for (let i = 0; i < arrCopy.length - 1; i++) {

src/plays/devblog/Pages/Article.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import axios from 'axios';
22
import { useState, useEffect } from 'react';
33
import { useParams } from 'react-router-dom';
4-
import sanitizeHTML from 'common/utils/sanitizeHTML';
54
import Loading from '../components/Loading';
5+
import sanitizeHTML from 'common/utils/sanitizeHTML';
66

77
const Article = () => {
88
const [article, setArticle] = useState({});

src/plays/fun-quiz/EndScreen.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// vendors
22
import { Fragment, useState } from 'react';
3+
34
import sanitizeHTML from 'common/utils/sanitizeHTML';
45

56
// css

src/plays/fun-quiz/QuizScreen.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useState, useCallback, useRef } from 'react';
2-
import sanitizeHTML from 'common/utils/sanitizeHTML';
32

3+
import sanitizeHTML from 'common/utils/sanitizeHTML';
44
import './QuizScreen.scss';
55

66
// assets

src/plays/markdown-editor/Output.jsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import React from 'react';
2-
import sanitizeHTML from 'common/utils/sanitizeHTML';
32

43
const Output = ({ md, text, mdPreviewBox }) => {
54
return (
65
<div
76
className="md-editor output-div"
8-
dangerouslySetInnerHTML={{ __html: sanitizeHTML(md.render(text)) }}
7+
dangerouslySetInnerHTML={{ __html: md.render(text) }}
98
id={mdPreviewBox}
109
/>
1110
);

src/plays/text-to-speech/TextToSpeech.jsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState, useEffect, useRef } from 'react';
22
import { FaVolumeUp, FaStop } from 'react-icons/fa';
33
import PlayHeader from 'common/playlists/PlayHeader';
4+
import sanitizeHTML from 'common/utils/sanitizeHTML';
45
import './styles.css';
56

67
function TextToSpeech(props) {
@@ -158,7 +159,10 @@ function TextToSpeech(props) {
158159
<div className="tts-output-box">
159160
{convertedText ? (
160161
<>
161-
<p className="tts-output-text">{convertedText}</p>
162+
<p
163+
className="tts-output-text"
164+
dangerouslySetInnerHTML={{ __html: sanitizeHTML(convertedText) }}
165+
/>
162166

163167
<button className="tts-speaker-btn" onClick={handleSpeak}>
164168
{isSpeaking ? <FaStop size={28} /> : <FaVolumeUp size={28} />}

0 commit comments

Comments
 (0)