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 ( +
+
+
+
+
    +
  • {intl.formatMessage(messages["footer.poweredby.text"])}
  • +
  • + + {intl.formatMessage( + +
  • +
  • + + {intl.formatMessage(messages["footer.logo.altText"])} + +
  • +
+
+ +
+ + {intl.formatMessage(messages["footer.copyright.text"])} + +
+
+ ); +}; diff --git a/tutorindigo/components/MobileViewHeader.jsx b/tutorindigo/components/MobileViewHeader.jsx new file mode 100644 index 000000000..152f53f8c --- /dev/null +++ b/tutorindigo/components/MobileViewHeader.jsx @@ -0,0 +1,36 @@ + +const MobileViewHeader = () => { + const config = getConfig(); + const intl = useIntl(); + const messages = { + "mobile.view.header.logo.altText": { + id: "mobile.view.header.logo.altText", + defaultMessage: "My Open edX", + description: "alt text for the mobile view header logo", + }, + }; + + const BASE_URL = config.LMS_BASE_URL; + + return ( + <> + + + {intl.formatMessage(messages["mobile.view.header.logo.altText"])} + {intl.formatMessage(messages["mobile.view.header.logo.altText"])} + + + ); +}; 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 ( + <> + + + Open edX + Open edX + + + ); +}; 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 ( +
+
+ +
+
+ +
+
+ +
+
+ ); +}; diff --git a/tutorindigo/patches/mfe-env-config-buildtime-definitions b/tutorindigo/patches/mfe-env-config-buildtime-definitions index 552325f5c..1d1fc3811 100644 --- a/tutorindigo/patches/mfe-env-config-buildtime-definitions +++ b/tutorindigo/patches/mfe-env-config-buildtime-definitions @@ -1,52 +1 @@ -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 (
); -}; +{{- patch("AddDarkTheme.jsx") }} diff --git a/tutorindigo/patches/mfe-env-config-buildtime-imports b/tutorindigo/patches/mfe-env-config-buildtime-imports index ebc974f0a..efb5b1222 100644 --- a/tutorindigo/patches/mfe-env-config-buildtime-imports +++ b/tutorindigo/patches/mfe-env-config-buildtime-imports @@ -1,7 +1 @@ -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'; +{{- patch("Imports.jsx") }} diff --git a/tutorindigo/patches/mfe-env-config-runtime-definitions b/tutorindigo/patches/mfe-env-config-runtime-definitions index 323d313d2..63c625660 100644 --- a/tutorindigo/patches/mfe-env-config-runtime-definitions +++ b/tutorindigo/patches/mfe-env-config-runtime-definitions @@ -1,265 +1,4 @@ -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 ( -
-
-
-
-
    -
  • {intl.formatMessage(messages["footer.poweredby.text"])}
  • -
  • - - {intl.formatMessage( - -
  • -
  • - - {intl.formatMessage(messages["footer.logo.altText"])} - -
  • -
-
- -
- - {intl.formatMessage(messages["footer.copyright.text"])} - -
-
- ); -}; - -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 ( -
-
- -
-
- -
-
- -
-
- ); -}; - -const MobileViewHeader = () => { - const config = getConfig(); - const intl = useIntl(); - const messages = { - "mobile.view.header.logo.altText": { - id: "mobile.view.header.logo.altText", - defaultMessage: "My Open edX", - description: "Mobile view header logo altText", - }, - }; - return ( -
- - - {intl.formatMessage(messages["mobile.view.header.logo.altText"])} - {intl.formatMessage(messages["mobile.view.header.logo.altText"])} - - -
- ); -}; - -const ThemedLogo = () => { - const BASE_URL = getConfig().LMS_BASE_URL; - - return ( - <> - - - Open edX - Open edX - - - ); -}; +{{- 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,