Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/common/badges-dashboard/BadgeDetails.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Badge from './Badge';
import sanitizeHTML from 'common/utils/sanitizeHTML';
import './badge.css';
import sanitizeHTML from 'common/utils/sanitizeHTML';

const BadgeDetails = ({ badge, onClose }) => {
const makeClickableLinks = (badge) => {
Expand Down
60 changes: 60 additions & 0 deletions src/common/playlists/PlayErrorBoundary.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
.play-error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 1.5rem;
text-align: center;
min-height: 50vh;
}

.play-error-image {
width: 200px;
height: auto;
margin-bottom: 1.5rem;
opacity: 0.8;
}

.play-error-title {
font-size: 1.5rem;
font-weight: 600;
color: #333;
margin: 0 0 0.75rem;
}

.play-error-message {
font-size: 1rem;
color: #666;
max-width: 500px;
line-height: 1.5;
margin: 0 0 1.5rem;
}

.play-error-actions {
display: flex;
gap: 1rem;
}

.play-error-retry-button {
padding: 0.6rem 1.5rem;
font-size: 0.95rem;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
background: #00f2fe;
color: #fff;
transition: opacity 0.2s;
}

.play-error-back-button {
padding: 0.6rem 1.5rem;
font-size: 0.95rem;
font-weight: 600;
border: 2px solid #00f2fe;
border-radius: 6px;
cursor: pointer;
background: transparent;
color: #00f2fe;
transition: opacity 0.2s;
}
64 changes: 64 additions & 0 deletions src/common/playlists/PlayErrorBoundary.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { ReactComponent as ImageOops } from 'images/img-oops.svg';
import './PlayErrorBoundary.css';

class PlayErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, isChunkError: false };
}

static getDerivedStateFromError(error) {
// Detect chunk load failures (network errors loading lazy chunks)
const isChunkError =
error?.name === 'ChunkLoadError' ||
/loading chunk/i.test(error?.message) ||
/failed to fetch dynamically imported module/i.test(error?.message);

return { hasError: true, error, isChunkError };
}

componentDidCatch(error, errorInfo) {
console.error(`Error loading play "${this.props.playName}":`, error, errorInfo);
}

handleRetry = () => {
this.setState({ hasError: false, error: null, isChunkError: false });
};

handleGoBack = () => {
window.location.href = '/plays';
};

render() {
if (this.state.hasError) {
return (
<div className="play-error-boundary play-error-container">
<ImageOops className="play-error-image" />
<h2 className="play-error-title">
{this.state.isChunkError ? 'Failed to load this play' : 'Something went wrong'}
</h2>
<p className="play-error-message">
{this.state.isChunkError
? 'There was a network error loading this play. Please check your connection and try again.'
: `An error occurred while rendering "${this.props.playName || 'this play'}".`}
</p>
<div className="play-error-actions">
{this.state.isChunkError && (
<button className="play-error-retry-button" onClick={this.handleRetry}>
Retry
</button>
)}
<button className="play-error-back-button" onClick={this.handleGoBack}>
Back to Plays
</button>
</div>
</div>
);
}

return this.props.children;
}
}

export default PlayErrorBoundary;
11 changes: 10 additions & 1 deletion src/common/playlists/PlayMeta.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PageNotFound } from 'common';
import thumbPlay from 'images/thumb-play.png';
import { getProdUrl } from 'common/utils/commonUtils';
import { loadCoverImage } from 'common/utils/coverImageUtil';
import PlayErrorBoundary from 'common/playlists/PlayErrorBoundary';

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

if (!Comp) {
return <PageNotFound />;
}

return <Comp {...play} />;
};

Expand All @@ -103,7 +108,11 @@ function PlayMeta() {
<meta content={play.description} data-react-helmet="true" name="twitter:description" />
<meta content={ogTagImage} data-react-helmet="true" name="twitter:image" />
</Helmet>
<Suspense fallback={<Loader />}>{renderPlayComponent()}</Suspense>
<Suspense
fallback={<Loader subtitle="Please wait while the play loads" title="Loading Play..." />}
>
<PlayErrorBoundary playName={play.name}>{renderPlayComponent()}</PlayErrorBoundary>
</Suspense>
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ function SelectionSortVisualizer() {
const handleSort = async () => {
const arrCopy = [...arr];
const outputElements = document.getElementById('output-visualizer');
// Safe: clears the container to empty string (no user data injected).
// All subsequent DOM mutations use createElement/createTextNode (XSS-safe).
outputElements.innerHTML = '';

for (let i = 0; i < arrCopy.length - 1; i++) {
Expand Down
2 changes: 1 addition & 1 deletion src/plays/devblog/Pages/Article.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import axios from 'axios';
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import sanitizeHTML from 'common/utils/sanitizeHTML';
import Loading from '../components/Loading';
import sanitizeHTML from 'common/utils/sanitizeHTML';

const Article = () => {
const [article, setArticle] = useState({});
Expand Down
1 change: 1 addition & 0 deletions src/plays/fun-quiz/EndScreen.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// vendors
import { Fragment, useState } from 'react';

import sanitizeHTML from 'common/utils/sanitizeHTML';

// css
Expand Down
2 changes: 1 addition & 1 deletion src/plays/fun-quiz/QuizScreen.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState, useCallback, useRef } from 'react';
import sanitizeHTML from 'common/utils/sanitizeHTML';

import sanitizeHTML from 'common/utils/sanitizeHTML';
import './QuizScreen.scss';

// assets
Expand Down
3 changes: 1 addition & 2 deletions src/plays/markdown-editor/Output.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React from 'react';
import sanitizeHTML from 'common/utils/sanitizeHTML';

const Output = ({ md, text, mdPreviewBox }) => {
return (
<div
className="md-editor output-div"
dangerouslySetInnerHTML={{ __html: sanitizeHTML(md.render(text)) }}
dangerouslySetInnerHTML={{ __html: md.render(text) }}
id={mdPreviewBox}
/>
);
Expand Down
6 changes: 5 additions & 1 deletion src/plays/text-to-speech/TextToSpeech.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { FaVolumeUp, FaStop } from 'react-icons/fa';
import PlayHeader from 'common/playlists/PlayHeader';
import sanitizeHTML from 'common/utils/sanitizeHTML';
import './styles.css';

function TextToSpeech(props) {
Expand Down Expand Up @@ -158,7 +159,10 @@ function TextToSpeech(props) {
<div className="tts-output-box">
{convertedText ? (
<>
<p className="tts-output-text">{convertedText}</p>
<p
className="tts-output-text"
dangerouslySetInnerHTML={{ __html: sanitizeHTML(convertedText) }}
/>

<button className="tts-speaker-btn" onClick={handleSpeak}>
{isSpeaking ? <FaStop size={28} /> : <FaVolumeUp size={28} />}
Expand Down
4 changes: 2 additions & 2 deletions src/plays/tube2tunes/Tube2tunes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ function Tube2tunes(props) {
.catch((err) => {
setError(true);
setLoading(false);
// eslint-disable-next-line no-console
console.error('Error: ', err);
// Optional: log error for debugging
console.error('Error fetching YouTube audio:', err);
});

inputUrlRef.current.value = '';
Expand Down
Loading