diff --git a/changelog.d/20260216_132943_faraz.maqsood_refactor_and_simplify_indigo_patches_structure_for_react_components.md b/changelog.d/20260216_132943_faraz.maqsood_refactor_and_simplify_indigo_patches_structure_for_react_components.md new file mode 100644 index 000000000..94d9197fa --- /dev/null +++ b/changelog.d/20260216_132943_faraz.maqsood_refactor_and_simplify_indigo_patches_structure_for_react_components.md @@ -0,0 +1 @@ +- [Improvement] Refactor indigo patches and separate react components from patches folder and render them cleanly. This also improves the development of react components in jsx files instead of patches. (by @Faraz32123) \ No newline at end of file diff --git a/tutorindigo/components/AddDarkTheme.jsx b/tutorindigo/components/AddDarkTheme.jsx new file mode 100644 index 000000000..f2c1e4f25 --- /dev/null +++ b/tutorindigo/components/AddDarkTheme.jsx @@ -0,0 +1,53 @@ + +let themeVariant = 'selected-paragon-theme-variant'; + +const AddDarkTheme = () => { + const isThemeToggleEnabled = getConfig().INDIGO_ENABLE_DARK_TOGGLE; + + const addDarkThemeToIframes = () => { + const iframes = document.getElementsByTagName('iframe'); + const iframesLength = iframes.length; + if (iframesLength > 0) { + Array.from({ length: iframesLength }).forEach((_, index) => { + const style = document.createElement('style'); + style.textContent = ` + body { + background-color: #0D0D0E; + color: #ccc; + } + a { color: #ccc; } + a:hover { color: #d3d3d3; } + `; + if (iframes[index].contentDocument) { + iframes[index].contentDocument.head.appendChild(style); + } + }); + } + }; + + useEffect(() => { + const theme = window.localStorage.getItem(themeVariant); + + // - When page loads, Footer loads before MFE content. Since there is no iframe on page, + // it does not append any class. MutationObserver observes changes in DOM and hence appends dark + // attributes when iframe is added. After 15 sec, this observer is destroyed to conserve resources. + // - It has been added outside dark-theme condition so that it can be removed on Component Unmount. + // - Observer can be passed to `addDarkThemeToIframes` function and disconnected after observing Iframe. + // This approach has a limitation: the observer first detects the iframe and then detects the docSrc. + // We need to wait for docSrc to fully load before appending the style tag. + const observer = new MutationObserver(() => { + addDarkThemeToIframes(); + }); + + if (isThemeToggleEnabled && theme === 'dark') { + document.documentElement.setAttribute('data-paragon-theme-variant', 'dark'); + + observer.observe(document.body, { childList: true, subtree: true }); + setTimeout(() => observer?.disconnect(), 15000); // clear after 15 sec to avoid resource usage + } + + return () => observer?.disconnect(); + }, []); + + return (
); +}; diff --git a/tutorindigo/components/Example.jsx b/tutorindigo/components/Example.jsx new file mode 100644 index 000000000..c575e50aa --- /dev/null +++ b/tutorindigo/components/Example.jsx @@ -0,0 +1,4 @@ + +// Add your imports in tutorindigo/components/imports.jsx. +// Add your component in tutorindigo/components/Example.jsx. +// Declare your component in tutorindigo/patches/mfe-env-config-runtime-definitions e.g. {{- patch("Example.jsx") }}. diff --git a/tutorindigo/components/Imports.jsx b/tutorindigo/components/Imports.jsx new file mode 100644 index 000000000..ebc974f0a --- /dev/null +++ b/tutorindigo/components/Imports.jsx @@ -0,0 +1,7 @@ +import React, { useEffect, useState } from 'react'; +import Cookies from 'universal-cookie'; + +import { getConfig } from '@edx/frontend-platform'; +import { Icon } from '@openedx/paragon'; +import { Nightlight, WbSunny } from '@openedx/paragon/icons'; +import { useIntl } from '@edx/frontend-platform/i18n'; diff --git a/tutorindigo/components/IndigoFooter.jsx b/tutorindigo/components/IndigoFooter.jsx new file mode 100644 index 000000000..f5f3ebf4c --- /dev/null +++ b/tutorindigo/components/IndigoFooter.jsx @@ -0,0 +1,80 @@ + +const IndigoFooter = () => { + const intl = useIntl(); + const config = getConfig(); + + const indigoFooterNavLinks = config.INDIGO_FOOTER_NAV_LINKS || []; + + const messages = { + "footer.poweredby.text": { + id: "footer.poweredby.text", + defaultMessage: "Powered by", + description: "text for the footer", + }, + "footer.tutorlogo.altText": { + id: "footer.tutorlogo.altText", + defaultMessage: "Runs on Tutor", + description: "alt text for the footer tutor logo", + }, + "footer.logo.altText": { + id: "footer.logo.altText", + defaultMessage: "Powered by Open edX", + description: "alt text for the footer logo.", + }, + "footer.copyright.text": { + id: "footer.copyright.text", + defaultMessage: `Copyrights ©${new Date().getFullYear()}. All Rights Reserved.`, + description: "copyright text for the footer", + }, + }; + + return ( +
+
+
+ >
+ );
+};
diff --git a/tutorindigo/components/ThemedLogo.jsx b/tutorindigo/components/ThemedLogo.jsx
new file mode 100644
index 000000000..7ff6ccb13
--- /dev/null
+++ b/tutorindigo/components/ThemedLogo.jsx
@@ -0,0 +1,26 @@
+
+const ThemedLogo = () => {
+ const BASE_URL = getConfig().LMS_BASE_URL;
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
diff --git a/tutorindigo/components/ToggleThemeButton.jsx b/tutorindigo/components/ToggleThemeButton.jsx
new file mode 100644
index 000000000..1561826d9
--- /dev/null
+++ b/tutorindigo/components/ToggleThemeButton.jsx
@@ -0,0 +1,111 @@
+
+const ToggleThemeButton = () => {
+ const intl = useIntl();
+ const [isDarkThemeEnabled, setIsDarkThemeEnabled] = useState(false);
+
+ const themeCookie = 'selected-paragon-theme-variant';
+ const themeCookieExpiry = 90; // days
+ const isThemeToggleEnabled = getConfig().INDIGO_ENABLE_DARK_TOGGLE;
+
+ const getCookie = (name) => {
+ return document.cookie
+ .split("; ")
+ .find((row) => row.startsWith(name + "="))
+ ?.split("=")[1];
+ };
+
+ const setCookie = (name, value, { domain, path, expires }) => {
+ document.cookie = `${name}=${value}; domain=${domain}; path=${path}; expires=${expires.toUTCString()}; SameSite=Lax`;
+ };
+
+ const serverURL = new URL(getConfig().LMS_BASE_URL);
+
+ const getCookieExpiry = () => {
+ const today = new Date();
+ return new Date(
+ today.getFullYear(),
+ today.getMonth(),
+ today.getDate() + themeCookieExpiry
+ );
+ };
+
+ const getCookieOptions = (serverURL) => ({
+ domain: serverURL.hostname,
+ path: '/',
+ expires: getCookieExpiry(),
+ });
+
+ const onToggleTheme = () => {
+ let theme = '';
+
+ if (getCookie(themeCookie) === 'dark') {
+ document.documentElement.setAttribute('data-paragon-theme-variant', 'light');
+ setIsDarkThemeEnabled(false);
+ theme = 'light';
+ } else {
+ document.documentElement.setAttribute('data-paragon-theme-variant', 'dark');
+ setIsDarkThemeEnabled(true);
+ theme = 'dark';
+ }
+
+ window.localStorage.setItem(themeCookie, theme);
+ setTimeout(() => {
+ setCookie(themeCookie, theme, getCookieOptions(serverURL));
+ window.location.reload();
+ }, 1);
+ };
+
+ useEffect(() => {
+ if (!getCookie(themeCookie) || getCookie(themeCookie) === 'undefined') {
+ return;
+ }
+ if (getCookie(themeCookie) !== window.localStorage.getItem(themeCookie)) {
+ window.localStorage.setItem(themeCookie, getCookie(themeCookie));
+ window.location.reload();
+ }
+ }, []);
+
+ const handleKeyUp = (event) => {
+ if (event.key === "Enter") {
+ onToggleTheme();
+ }
+ };
+
+ if (!isThemeToggleEnabled) {
+ return ;
+ }
+
+ const messages = {
+ "header.user.theme": {
+ id: "header.user.theme",
+ defaultMessage: "Toggle Theme",
+ description: "Toggle between light and dark theme",
+ },
+ };
+
+ return (
+
-
-
-
-
-
- >
- );
-};
+{{- patch("IndigoFooter.jsx") }}
+{{- patch("MobileViewHeader.jsx") }}
+{{- patch("ThemedLogo.jsx") }}
+{{- patch("ToggleThemeButton.jsx") }}
diff --git a/tutorindigo/plugin.py b/tutorindigo/plugin.py
index ff085771f..e49f52333 100644
--- a/tutorindigo/plugin.py
+++ b/tutorindigo/plugin.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import itertools
import json
import os
import typing as t
@@ -176,21 +177,18 @@ def _override_openedx_docker_image(
)
-# Apply patches from tutor-indigo
-for path in glob(
- os.path.join(
- str(importlib_resources.files("tutorindigo") / "patches"),
- "*",
- )
+# Add react components and patches from tutor-indigo
+for path in itertools.chain(
+ glob(
+ os.path.join(str(importlib_resources.files("tutorindigo") / "components"), "*")
+ ),
+ glob(os.path.join(str(importlib_resources.files("tutorindigo") / "patches"), "*")),
):
with open(path, encoding="utf-8") as patch_file:
hooks.Filters.ENV_PATCHES.add_item((os.path.basename(path), patch_file.read()))
for mfe in indigo_styled_mfes:
- # TODO: move plugins from these patches(mfe-env-config-buildtime-definitions,
- # mfe-env-config-runtime-definitions) into separate files and generate these
- # patches on the fly to improve readability.
PLUGIN_SLOTS.add_item(
(
mfe,