Skip to content

Commit aab46e3

Browse files
Muhammad Faraz  MaqsoodMuhammad Faraz  Maqsood
authored andcommitted
feat: refactor & make indigo patches more readable
- refactor, simplify & make indigo patches more readable - separate react components from patches folder and render them cleanly, this also improve development of react components in jsx files instead of patches.
1 parent ef43b53 commit aab46e3

12 files changed

+331
-333
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
2+
let themeVariant = 'selected-paragon-theme-variant';
3+
4+
const AddDarkTheme = () => {
5+
const isThemeToggleEnabled = getConfig().INDIGO_ENABLE_DARK_TOGGLE;
6+
7+
const addDarkThemeToIframes = () => {
8+
const iframes = document.getElementsByTagName('iframe');
9+
const iframesLength = iframes.length;
10+
if (iframesLength > 0) {
11+
Array.from({ length: iframesLength }).forEach((_, index) => {
12+
const style = document.createElement('style');
13+
style.textContent = `
14+
body {
15+
background-color: #0D0D0E;
16+
color: #ccc;
17+
}
18+
a { color: #ccc; }
19+
a:hover { color: #d3d3d3; }
20+
`;
21+
if (iframes[index].contentDocument) {
22+
iframes[index].contentDocument.head.appendChild(style);
23+
}
24+
});
25+
}
26+
};
27+
28+
useEffect(() => {
29+
const theme = window.localStorage.getItem(themeVariant);
30+
31+
// - When page loads, Footer loads before MFE content. Since there is no iframe on page,
32+
// it does not append any class. MutationObserver observes changes in DOM and hence appends dark
33+
// attributes when iframe is added. After 15 sec, this observer is destroyed to conserve resources.
34+
// - It has been added outside dark-theme condition so that it can be removed on Component Unmount.
35+
// - Observer can be passed to `addDarkThemeToIframes` function and disconnected after observing Iframe.
36+
// This approach has a limitation: the observer first detects the iframe and then detects the docSrc.
37+
// We need to wait for docSrc to fully load before appending the style tag.
38+
const observer = new MutationObserver(() => {
39+
addDarkThemeToIframes();
40+
});
41+
42+
if (isThemeToggleEnabled && theme === 'dark') {
43+
document.documentElement.setAttribute('data-paragon-theme-variant', 'dark');
44+
45+
observer.observe(document.body, { childList: true, subtree: true });
46+
setTimeout(() => observer?.disconnect(), 15000); // clear after 15 sec to avoid resource usage
47+
}
48+
49+
return () => observer?.disconnect();
50+
}, []);
51+
52+
return (<div />);
53+
};

tutorindigo/components/Example.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
// Add your imports in tutorindigo/components/imports.jsx.
3+
// Add your component in tutorindigo/components/Example.jsx.
4+
// Declare your component in tutorindigo/patches/mfe-env-config-runtime-definitions e.g. {{- patch("Example.jsx") }}.

tutorindigo/components/Imports.jsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React, { useEffect, useState } from 'react';
2+
import Cookies from 'universal-cookie';
3+
4+
import { getConfig } from '@edx/frontend-platform';
5+
import { Icon } from '@openedx/paragon';
6+
import { Nightlight, WbSunny } from '@openedx/paragon/icons';
7+
import { useIntl } from '@edx/frontend-platform/i18n';
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
2+
const IndigoFooter = () => {
3+
const intl = useIntl();
4+
const config = getConfig();
5+
6+
const indigoFooterNavLinks = config.INDIGO_FOOTER_NAV_LINKS || [];
7+
8+
const messages = {
9+
"footer.poweredby.text": {
10+
id: "footer.poweredby.text",
11+
defaultMessage: "Powered by",
12+
description: "text for the footer",
13+
},
14+
"footer.tutorlogo.altText": {
15+
id: "footer.tutorlogo.altText",
16+
defaultMessage: "Runs on Tutor",
17+
description: "alt text for the footer tutor logo",
18+
},
19+
"footer.logo.altText": {
20+
id: "footer.logo.altText",
21+
defaultMessage: "Powered by Open edX",
22+
description: "alt text for the footer logo.",
23+
},
24+
"footer.copyright.text": {
25+
id: "footer.copyright.text",
26+
defaultMessage: `Copyrights ©${new Date().getFullYear()}. All Rights Reserved.`,
27+
description: "copyright text for the footer",
28+
},
29+
};
30+
31+
return (
32+
<div className="wrapper wrapper-footer">
33+
<footer id="footer" className="tutor-container">
34+
<div className="footer-top">
35+
<div className="powered-area">
36+
<ul className="logo-list">
37+
<li>{intl.formatMessage(messages["footer.poweredby.text"])}</li>
38+
<li>
39+
<a
40+
href="https://edly.io/tutor/"
41+
rel="noreferrer"
42+
target="_blank"
43+
>
44+
<img
45+
src={`${config.LMS_BASE_URL}/theming/asset/images/tutor-logo.png`}
46+
alt={intl.formatMessage(
47+
messages["footer.tutorlogo.altText"]
48+
)}
49+
width="57"
50+
/>
51+
</a>
52+
</li>
53+
<li>
54+
<a href="https://open.edx.org" rel="noreferrer" target="_blank">
55+
<img
56+
src={`${config.LMS_BASE_URL}/theming/asset/images/openedx-logo.png`}
57+
alt={intl.formatMessage(messages["footer.logo.altText"])}
58+
width="79"
59+
/>
60+
</a>
61+
</li>
62+
</ul>
63+
</div>
64+
<nav className="nav-colophon">
65+
<ol>
66+
{indigoFooterNavLinks.map((link) => (
67+
<li key={link.url}>
68+
<a href={`${config.LMS_BASE_URL}${link.url}`}>{link.title}</a>
69+
</li>
70+
))}
71+
</ol>
72+
</nav>
73+
</div>
74+
<span className="copyright-site">
75+
{intl.formatMessage(messages["footer.copyright.text"])}
76+
</span>
77+
</footer>
78+
</div>
79+
);
80+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
const MobileViewHeader = () => {
3+
const config = getConfig();
4+
const intl = useIntl();
5+
const messages = {
6+
"mobile.view.header.logo.altText": {
7+
id: "mobile.view.header.logo.altText",
8+
defaultMessage: "My Open edX",
9+
description: "alt text for the mobile view header logo",
10+
},
11+
};
12+
13+
const BASE_URL = config.LMS_BASE_URL;
14+
15+
return (
16+
<>
17+
<style>
18+
{`
19+
#root header .logo-image.logo-white {
20+
display: none;
21+
}
22+
[data-paragon-theme-variant="dark"] #root header .logo-image {
23+
display: none;
24+
}
25+
[data-paragon-theme-variant="dark"] #root header .logo-white {
26+
display: block;
27+
}
28+
`}
29+
</style>
30+
<a href={`${BASE_URL}/dashboard`} title="Open edX" className="logo">
31+
<img className="logo-image" src={`${BASE_URL}/static/indigo/images/logo.png`} alt={intl.formatMessage(messages["mobile.view.header.logo.altText"])} />
32+
<img className="logo-image logo-white" src={`${BASE_URL}/static/indigo/images/logo-white.png`} alt={intl.formatMessage(messages["mobile.view.header.logo.altText"])} />
33+
</a>
34+
</>
35+
);
36+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
const ThemedLogo = () => {
3+
const BASE_URL = getConfig().LMS_BASE_URL;
4+
5+
return (
6+
<>
7+
<style>
8+
{`
9+
#root header .logo-image.logo-white {
10+
display: none;
11+
}
12+
[data-paragon-theme-variant="dark"] #root header .logo-image {
13+
display: none;
14+
}
15+
[data-paragon-theme-variant="dark"] #root header .logo-white {
16+
display: block;
17+
}
18+
`}
19+
</style>
20+
<a href={`${BASE_URL}/dashboard`} title="Open edX" className="logo">
21+
<img className="logo-image" src={`${BASE_URL}/static/indigo/images/logo.png`} alt="Open edX" />
22+
<img className="logo-image logo-white" src={`${BASE_URL}/static/indigo/images/logo-white.png`} alt="Open edX" />
23+
</a>
24+
</>
25+
);
26+
};
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
2+
const ToggleThemeButton = () => {
3+
const intl = useIntl();
4+
const [isDarkThemeEnabled, setIsDarkThemeEnabled] = useState(false);
5+
6+
const themeCookie = 'selected-paragon-theme-variant';
7+
const themeCookieExpiry = 90; // days
8+
const isThemeToggleEnabled = getConfig().INDIGO_ENABLE_DARK_TOGGLE;
9+
10+
const getCookie = (name) => {
11+
return document.cookie
12+
.split("; ")
13+
.find((row) => row.startsWith(name + "="))
14+
?.split("=")[1];
15+
};
16+
17+
const setCookie = (name, value, { domain, path, expires }) => {
18+
document.cookie = `${name}=${value}; domain=${domain}; path=${path}; expires=${expires.toUTCString()}; SameSite=Lax`;
19+
};
20+
21+
const serverURL = new URL(getConfig().LMS_BASE_URL);
22+
23+
const getCookieExpiry = () => {
24+
const today = new Date();
25+
return new Date(
26+
today.getFullYear(),
27+
today.getMonth(),
28+
today.getDate() + themeCookieExpiry
29+
);
30+
};
31+
32+
const getCookieOptions = (serverURL) => ({
33+
domain: serverURL.hostname,
34+
path: '/',
35+
expires: getCookieExpiry(),
36+
});
37+
38+
const onToggleTheme = () => {
39+
let theme = '';
40+
41+
if (getCookie(themeCookie) === 'dark') {
42+
document.documentElement.setAttribute('data-paragon-theme-variant', 'light');
43+
setIsDarkThemeEnabled(false);
44+
theme = 'light';
45+
} else {
46+
document.documentElement.setAttribute('data-paragon-theme-variant', 'dark');
47+
setIsDarkThemeEnabled(true);
48+
theme = 'dark';
49+
}
50+
51+
window.localStorage.setItem(themeCookie, theme);
52+
setTimeout(() => {
53+
setCookie(themeCookie, theme, getCookieOptions(serverURL));
54+
window.location.reload();
55+
}, 1);
56+
};
57+
58+
useEffect(() => {
59+
if (!getCookie(themeCookie) || getCookie(themeCookie) === 'undefined') {
60+
return;
61+
}
62+
if (getCookie(themeCookie) !== window.localStorage.getItem(themeCookie)) {
63+
window.localStorage.setItem(themeCookie, getCookie(themeCookie));
64+
window.location.reload();
65+
}
66+
}, []);
67+
68+
const handleKeyUp = (event) => {
69+
if (event.key === "Enter") {
70+
onToggleTheme();
71+
}
72+
};
73+
74+
if (!isThemeToggleEnabled) {
75+
return <div />;
76+
}
77+
78+
const messages = {
79+
"header.user.theme": {
80+
id: "header.user.theme",
81+
defaultMessage: "Toggle Theme",
82+
description: "Toggle between light and dark theme",
83+
},
84+
};
85+
86+
return (
87+
<div className="theme-toggle-button mr-3">
88+
<div className="light-theme-icon">
89+
<Icon src={WbSunny} />
90+
</div>
91+
<div className="toggle-switch">
92+
<label htmlFor="theme-toggle-checkbox" className="switch">
93+
<input
94+
id="theme-toggle-checkbox"
95+
defaultChecked={getCookie(themeCookie) === "dark"}
96+
onChange={onToggleTheme}
97+
onKeyUp={handleKeyUp}
98+
type="checkbox"
99+
title={intl.formatMessage(messages["header.user.theme"])}
100+
/>
101+
<span className="slider round" />
102+
<span id="theme-label" className="sr-only">{`Switch to ${isDarkThemeEnabled ? "Light" : "Dark"
103+
} Mode`}</span>
104+
</label>
105+
</div>
106+
<div className="dark-theme-icon">
107+
<Icon src={Nightlight} />
108+
</div>
109+
</div>
110+
);
111+
};
Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1 @@
1-
let themeVariant = 'selected-paragon-theme-variant';
2-
3-
const AddDarkTheme = () => {
4-
const isThemeToggleEnabled = getConfig().INDIGO_ENABLE_DARK_TOGGLE;
5-
6-
const addDarkThemeToIframes = () => {
7-
const iframes = document.getElementsByTagName('iframe');
8-
const iframesLength = iframes.length;
9-
if (iframesLength > 0) {
10-
Array.from({ length: iframesLength }).forEach((_, index) => {
11-
const style = document.createElement('style');
12-
style.textContent = `
13-
body {
14-
background-color: #0D0D0E;
15-
color: #ccc;
16-
}
17-
a { color: #ccc; }
18-
a:hover { color: #d3d3d3; }
19-
`;
20-
if (iframes[index].contentDocument) {
21-
iframes[index].contentDocument.head.appendChild(style);
22-
}
23-
});
24-
}
25-
};
26-
27-
useEffect(() => {
28-
const theme = window.localStorage.getItem(themeVariant);
29-
30-
// - When page loads, Footer loads before MFE content. Since there is no iframe on page,
31-
// it does not append any class. MutationObserver observes changes in DOM and hence appends dark
32-
// attributes when iframe is added. After 15 sec, this observer is destroyed to conserve resources.
33-
// - It has been added outside dark-theme condition so that it can be removed on Component Unmount.
34-
// - Observer can be passed to `addDarkThemeToIframes` function and disconnected after observing Iframe.
35-
// This approach has a limitation: the observer first detects the iframe and then detects the docSrc.
36-
// We need to wait for docSrc to fully load before appending the style tag.
37-
const observer = new MutationObserver(() => {
38-
addDarkThemeToIframes();
39-
});
40-
41-
if (isThemeToggleEnabled && theme === 'dark') {
42-
document.documentElement.setAttribute('data-paragon-theme-variant', 'dark');
43-
44-
observer.observe(document.body, { childList: true, subtree: true });
45-
setTimeout(() => observer?.disconnect(), 15000); // clear after 15 sec to avoid resource usage
46-
}
47-
48-
return () => observer?.disconnect();
49-
}, []);
50-
51-
return (<div />);
52-
};
1+
{{- patch("AddDarkTheme.jsx") }}
Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1 @@
1-
import React, { useEffect, useState } from 'react';
2-
import Cookies from 'universal-cookie';
3-
4-
import { getConfig } from '@edx/frontend-platform';
5-
import { Icon } from '@openedx/paragon';
6-
import { Nightlight, WbSunny } from '@openedx/paragon/icons';
7-
import { useIntl } from '@edx/frontend-platform/i18n';
1+
{{- patch("Imports.jsx") }}

0 commit comments

Comments
 (0)