diff --git a/.husky/pre-commit b/.husky/pre-commit
index 0312b76025..7e2936624c 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
-npx lint-staged
\ No newline at end of file
+yarn pre-commit
\ No newline at end of file
diff --git a/src/common/Testimonial/TestimonialCard.jsx b/src/common/Testimonial/TestimonialCard.jsx
index 9bf94226a0..c2e0c29ce0 100644
--- a/src/common/Testimonial/TestimonialCard.jsx
+++ b/src/common/Testimonial/TestimonialCard.jsx
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
import { format } from 'date-fns';
import * as allLocales from 'date-fns/locale';
import { email2Slug } from 'common/services/string';
-import sanitizeHTML from 'common/utils/sanitizeHTML';
const TestimonialCard = ({ home, quote, name, avatarUrl, category, created_at, email }) => {
const [formattedDate] = useState(() => {
@@ -60,7 +59,7 @@ const TestimonialCard = ({ home, quote, name, avatarUrl, category, created_at, e
>
diff --git a/src/common/badges-dashboard/BadgeDetails.jsx b/src/common/badges-dashboard/BadgeDetails.jsx
index ff78258d9f..893bf60b19 100644
--- a/src/common/badges-dashboard/BadgeDetails.jsx
+++ b/src/common/badges-dashboard/BadgeDetails.jsx
@@ -1,5 +1,4 @@
import Badge from './Badge';
-import sanitizeHTML from 'common/utils/sanitizeHTML';
import './badge.css';
const BadgeDetails = ({ badge, onClose }) => {
@@ -10,7 +9,7 @@ const BadgeDetails = ({ badge, onClose }) => {
return `${name}`;
});
- return ;
+ return ;
};
return (
diff --git a/src/common/playlists/PlayErrorBoundary.jsx b/src/common/playlists/PlayErrorBoundary.jsx
new file mode 100644
index 0000000000..f73e6f5bba
--- /dev/null
+++ b/src/common/playlists/PlayErrorBoundary.jsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import { ReactComponent as ImageOops } from 'images/img-oops.svg';
+
+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 (
+
+
+
+ {this.state.isChunkError ? 'Failed to load this play' : 'Something went wrong'}
+
+
+ {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'}".`}
+
+
+ {this.state.isChunkError && (
+
+ )}
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
+
+const styles = {
+ container: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: '3rem 1.5rem',
+ textAlign: 'center',
+ minHeight: '50vh'
+ },
+ image: {
+ width: '200px',
+ height: 'auto',
+ marginBottom: '1.5rem',
+ opacity: 0.8
+ },
+ title: {
+ fontSize: '1.5rem',
+ fontWeight: 600,
+ color: '#333',
+ margin: '0 0 0.75rem'
+ },
+ message: {
+ fontSize: '1rem',
+ color: '#666',
+ maxWidth: '500px',
+ lineHeight: 1.5,
+ margin: '0 0 1.5rem'
+ },
+ actions: {
+ display: 'flex',
+ gap: '1rem'
+ },
+ retryButton: {
+ padding: '0.6rem 1.5rem',
+ fontSize: '0.95rem',
+ fontWeight: 600,
+ border: 'none',
+ borderRadius: '6px',
+ cursor: 'pointer',
+ background: '#00f2fe',
+ color: '#fff',
+ transition: 'opacity 0.2s'
+ },
+ backButton: {
+ padding: '0.6rem 1.5rem',
+ fontSize: '0.95rem',
+ fontWeight: 600,
+ border: '2px solid #00f2fe',
+ borderRadius: '6px',
+ cursor: 'pointer',
+ background: 'transparent',
+ color: '#00f2fe',
+ transition: 'opacity 0.2s'
+ }
+};
+
+export default PlayErrorBoundary;
diff --git a/src/common/playlists/PlayMeta.jsx b/src/common/playlists/PlayMeta.jsx
index d5dbc2f17b..33158038d9 100644
--- a/src/common/playlists/PlayMeta.jsx
+++ b/src/common/playlists/PlayMeta.jsx
@@ -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);
@@ -87,6 +88,10 @@ function PlayMeta() {
const renderPlayComponent = () => {
const Comp = plays[play.component || toSanitized(play.title_name)];
+ if (!Comp) {
+ return ;
+ }
+
return ;
};
@@ -103,7 +108,11 @@ function PlayMeta() {
- }>{renderPlayComponent()}
+ }
+ >
+ {renderPlayComponent()}
+
>
);
}
diff --git a/src/common/utils/sanitizeHTML.js b/src/common/utils/sanitizeHTML.js
deleted file mode 100644
index b07766de01..0000000000
--- a/src/common/utils/sanitizeHTML.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import DOMPurify from 'dompurify';
-
-/**
- * Sanitizes an HTML string using DOMPurify to prevent XSS attacks.
- * Use this utility whenever you need to render dynamic HTML via dangerouslySetInnerHTML.
- *
- * @param {string} html - The raw HTML string to sanitize.
- * @returns {string} - A sanitized HTML string safe to use with dangerouslySetInnerHTML.
- */
-const sanitizeHTML = (html) => DOMPurify.sanitize(html ?? '');
-
-export default sanitizeHTML;
diff --git a/src/plays/Selection-Sort-Visualizer/SelectionSortVisualizer.js b/src/plays/Selection-Sort-Visualizer/SelectionSortVisualizer.js
index 1eb8241875..951f4380cb 100644
--- a/src/plays/Selection-Sort-Visualizer/SelectionSortVisualizer.js
+++ b/src/plays/Selection-Sort-Visualizer/SelectionSortVisualizer.js
@@ -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++) {
diff --git a/src/plays/devblog/Pages/Article.jsx b/src/plays/devblog/Pages/Article.jsx
index ef2ea2d2b0..8f7c70e7da 100644
--- a/src/plays/devblog/Pages/Article.jsx
+++ b/src/plays/devblog/Pages/Article.jsx
@@ -1,7 +1,6 @@
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';
const Article = () => {
@@ -51,7 +50,7 @@ const Article = () => {
) : (
diff --git a/src/plays/fun-quiz/EndScreen.jsx b/src/plays/fun-quiz/EndScreen.jsx
index 5398009a8b..a8f337760f 100644
--- a/src/plays/fun-quiz/EndScreen.jsx
+++ b/src/plays/fun-quiz/EndScreen.jsx
@@ -1,6 +1,5 @@
// vendors
import { Fragment, useState } from 'react';
-import sanitizeHTML from 'common/utils/sanitizeHTML';
// css
import './FrontScreen.scss';
@@ -17,17 +16,17 @@ const EndScreen = ({ quizSummary, redirectHome }) => {
Question: {currentQuestion?.qNo}
Ans: ${currentQuestion?.correct_answer}
`)
+ __html: `
Ans: ${currentQuestion?.correct_answer}
`
}}
/>
Your Answer: ${currentQuestion?.your_answer}`)
+ __html: `Your Answer: ${currentQuestion?.your_answer}`
}}
/>
diff --git a/src/plays/fun-quiz/QuizScreen.jsx b/src/plays/fun-quiz/QuizScreen.jsx
index 571946985a..cc150234f8 100644
--- a/src/plays/fun-quiz/QuizScreen.jsx
+++ b/src/plays/fun-quiz/QuizScreen.jsx
@@ -1,5 +1,4 @@
import { useEffect, useState, useCallback, useRef } from 'react';
-import sanitizeHTML from 'common/utils/sanitizeHTML';
import './QuizScreen.scss';
@@ -150,7 +149,7 @@ function QuizScreen({ category, getQuizSummary }) {
{timer}
Question: {questionNumber + 1}
-
+
{currentQuestion?.options?.map((option, index) => {
@@ -158,7 +157,7 @@ function QuizScreen({ category, getQuizSummary }) {
diff --git a/src/plays/markdown-editor/Output.jsx b/src/plays/markdown-editor/Output.jsx
index 8f7ca65314..9228271430 100644
--- a/src/plays/markdown-editor/Output.jsx
+++ b/src/plays/markdown-editor/Output.jsx
@@ -1,11 +1,10 @@
import React from 'react';
-import sanitizeHTML from 'common/utils/sanitizeHTML';
const Output = ({ md, text, mdPreviewBox }) => {
return (
);
diff --git a/src/plays/text-to-speech/TextToSpeech.jsx b/src/plays/text-to-speech/TextToSpeech.jsx
index e400bc4fde..3286a6d0f9 100644
--- a/src/plays/text-to-speech/TextToSpeech.jsx
+++ b/src/plays/text-to-speech/TextToSpeech.jsx
@@ -158,7 +158,10 @@ function TextToSpeech(props) {
{convertedText ? (
<>
-
{convertedText}
+