Skip to content
This repository was archived by the owner on Jan 28, 2026. It is now read-only.

Commit 55e8ddf

Browse files
authored
Merge pull request #1 from calculquebec/master
Merge main branch into cq/ulmo.dev
2 parents 1e65891 + 4553dda commit 55e8ddf

10 files changed

+363
-26
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,20 @@ instructions, because git commits are used to generate release notes:
1919

2020
<!-- scriv-insert-here -->
2121

22+
<a id='changelog-21.0.0'></a>
23+
24+
## v21.0.0 (2026-01-16)
25+
26+
- 💥[Feature] Upgrade to ulmo. (by @HammadYousaf01)
27+
28+
- 💥[Deprecation] Do not assign theme to preview site during initialization as the preview page has been migrated to the learning MFE. (by @Danyal-Faheem)
29+
30+
- [Improvement] Migrate from pylint and black to ruff. (by @HammadYousaf01)
31+
32+
- 💥[Feature] Add indigo header through pluginSlot instead of installing it through fork(edly-io/indigo-frontend-component-header). (by @Faraz32123, @HammadYousaf01)
33+
2234
<a id='changelog-20.0.1'></a>
35+
2336
## v20.0.1 (2025-09-23)
2437

2538
- [Improvement] Make latex response editor background colours WCAG compliant in dark theme. (by @Danyal-Faheem)

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ The indigo theme can’t override styles for MFEs directly. It overrides the sty
131131
))
132132

133133

134-
This Tutor plugin is maintained by Ahmed Khalid and Hammad Yousaf from `Edly <https://edly.io>`__. Community support is available from the official `Open edX forum <https://discuss.openedx.org>`__. Do you need help with this plugin? See the `troubleshooting <https://docs.tutor.edly.io/troubleshooting.html>`__ section from the Tutor documentation.
134+
This Tutor plugin is maintained by Muhammad Faraz Maqsood and Hammad Yousaf from `Edly <https://edly.io>`__. Community support is available from the official `Open edX forum <https://discuss.openedx.org>`__. Do you need help with this plugin? See the `troubleshooting <https://docs.tutor.edly.io/troubleshooting.html>`__ section from the Tutor documentation.
135135

136136

137137
License

changelog.d/20251028_185649_136305115+HammadYousaf01_ruff_migration.md

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- [Chore] Redirect to tutor page(https://edly.io/tutor/) instead of tutor docs(https://docs.tutor.edly.io) on tutor logo click in legacy pages footer. (by @Faraz32123)

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ license = { text = "AGPLv3" }
88
requires-python = ">=3.9"
99
authors = [{ name = "Edly" }, { email = "hello@edly.io" }]
1010
maintainers = [
11-
{ name = "Ahmed Khalid" }, { email = "ahmed.khalid@arbisoft.com" },
12-
{ name = "Hammad Yousaf" }, { email = "hammad.yousaf@arbisoft.com" }
11+
{ name = "Hammad Yousaf" }, { email = "hammad.yousaf@arbisoft.com" },
12+
{ name = "Muhammad Faraz Maqsood" }, { email = "faraz.maqsood@arbisoft.com" }
1313
]
1414
classifiers = [
1515
"Development Status :: 5 - Production/Stable",
@@ -23,16 +23,16 @@ classifiers = [
2323
"Programming Language :: Python :: 3.12",
2424
]
2525
dependencies = [
26-
"tutor-mfe>=20.0.0,<21.0.0",
27-
"tutor>=20.0.0,<21.0.0"
26+
"tutor-mfe>=21.0.0,<22.0.0",
27+
"tutor>=21.0.0,<22.0.0"
2828
]
2929

3030
# hatch_build.py will set it later
3131
dynamic = ["version"]
3232

3333
[project.optional-dependencies]
3434
dev = [
35-
"tutor[dev]>=20.0.0,<21.0.0",
35+
"tutor[dev]>=21.0.0,<22.0.0",
3636
"ruff",
3737
]
3838

tutorindigo/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "20.0.1"
1+
__version__ = "21.0.0"
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import React, { useEffect } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import Cookies from 'universal-cookie';
33

44
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: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
const IndigoFooter = () => {
2+
const intl = useIntl();
3+
const config = getConfig();
4+
5+
const indigoFooterNavLinks = config.INDIGO_FOOTER_NAV_LINKS || [];
6+
7+
const messages = {
8+
"footer.poweredby.text": {
9+
id: "footer.poweredby.text",
10+
defaultMessage: "Powered by",
11+
description: "text for the footer",
12+
},
13+
"footer.tutorlogo.altText": {
14+
id: "footer.tutorlogo.altText",
15+
defaultMessage: "Runs on Tutor",
16+
description: "alt text for the footer tutor logo",
17+
},
18+
"footer.logo.altText": {
19+
id: "footer.logo.altText",
20+
defaultMessage: "Powered by Open edX",
21+
description: "alt text for the footer logo.",
22+
},
23+
"footer.copyright.text": {
24+
id: "footer.copyright.text",
25+
defaultMessage: `Copyrights ©${new Date().getFullYear()}. All Rights Reserved.`,
26+
description: "copyright text for the footer",
27+
},
28+
};
29+
30+
return (
31+
<div className="wrapper wrapper-footer">
32+
<footer id="footer" className="tutor-container">
33+
<div className="footer-top">
34+
<div className="powered-area">
35+
<ul className="logo-list">
36+
<li>{intl.formatMessage(messages["footer.poweredby.text"])}</li>
37+
<li>
38+
<a
39+
href="https://edly.io/tutor/"
40+
rel="noreferrer"
41+
target="_blank"
42+
>
43+
<img
44+
src={`${config.LMS_BASE_URL}/theming/asset/images/tutor-logo.png`}
45+
alt={intl.formatMessage(
46+
messages["footer.tutorlogo.altText"]
47+
)}
48+
width="57"
49+
/>
50+
</a>
51+
</li>
52+
<li>
53+
<a href="https://open.edx.org" rel="noreferrer" target="_blank">
54+
<img
55+
src={`${config.LMS_BASE_URL}/theming/asset/images/openedx-logo.png`}
56+
alt={intl.formatMessage(messages["footer.logo.altText"])}
57+
width="79"
58+
/>
59+
</a>
60+
</li>
61+
</ul>
62+
</div>
63+
<nav className="nav-colophon">
64+
<ol>
65+
{indigoFooterNavLinks.map((link) => (
66+
<li key={link.url}>
67+
<a href={`${config.LMS_BASE_URL}${link.url}`}>{link.title}</a>
68+
</li>
69+
))}
70+
</ol>
71+
</nav>
72+
</div>
73+
<span className="copyright-site">
74+
{intl.formatMessage(messages["footer.copyright.text"])}
75+
</span>
76+
</footer>
77+
</div>
78+
);
79+
};
80+
81+
const ToggleThemeButton = () => {
82+
const intl = useIntl();
83+
const [isDarkThemeEnabled, setIsDarkThemeEnabled] = useState(false);
84+
85+
const themeCookie = "indigo-toggle-dark";
86+
const themeCookieExpiry = 90; // days
87+
const isThemeToggleEnabled = getConfig().INDIGO_ENABLE_DARK_TOGGLE;
88+
89+
const getCookie = (name) => {
90+
return document.cookie
91+
.split("; ")
92+
.find((row) => row.startsWith(name + "="))
93+
?.split("=")[1];
94+
};
95+
96+
const setCookie = (name, value, { domain, path, expires }) => {
97+
document.cookie = `${name}=${value}; domain=${domain}; path=${path}; expires=${expires.toUTCString()}; SameSite=Lax`;
98+
};
99+
100+
const getCookieExpiry = () => {
101+
const today = new Date();
102+
return new Date(
103+
today.getFullYear(),
104+
today.getMonth(),
105+
today.getDate() + themeCookieExpiry
106+
);
107+
};
108+
109+
const getCookieOptions = (serverURL) => ({
110+
domain: serverURL.hostname,
111+
path: "/",
112+
expires: getCookieExpiry(),
113+
});
114+
115+
const addDarkThemeToIframes = () => {
116+
const iframes = document.getElementsByTagName("iframe");
117+
const iframesLength = iframes.length;
118+
if (iframesLength > 0) {
119+
Array.from({ length: iframesLength }).forEach((_, ind) => {
120+
const style = document.createElement("style");
121+
style.textContent = `
122+
body{
123+
background-color: #0D0D0E;
124+
color: #ccc;
125+
}
126+
a {color: #ccc;}
127+
a:hover{color: #d3d3d3;}
128+
`;
129+
if (iframes[ind].contentDocument) {
130+
iframes[ind].contentDocument.head.appendChild(style);
131+
}
132+
});
133+
}
134+
};
135+
136+
const removeDarkThemeFromiframes = () => {
137+
const iframes = document.getElementsByTagName("iframe");
138+
const iframesLength = iframes.length;
139+
140+
Array.from({ length: iframesLength }).forEach((_, ind) => {
141+
if (iframes[ind].contentDocument) {
142+
const iframeHead = iframes[ind].contentDocument.head;
143+
const styleTag = Array.from(iframeHead.querySelectorAll("style")).find(
144+
(style) =>
145+
style.textContent.includes("background-color: #0D0D0E;") &&
146+
style.textContent.includes("color: #ccc;")
147+
);
148+
if (styleTag) {
149+
styleTag.remove();
150+
}
151+
}
152+
});
153+
};
154+
155+
const onToggleTheme = () => {
156+
const serverURL = new URL(getConfig().LMS_BASE_URL);
157+
let theme = "";
158+
159+
if (getCookie(themeCookie) === "dark") {
160+
document.body.classList.remove("indigo-dark-theme");
161+
removeDarkThemeFromiframes();
162+
setIsDarkThemeEnabled(false);
163+
theme = "light";
164+
} else {
165+
document.body.classList.add("indigo-dark-theme");
166+
addDarkThemeToIframes();
167+
setIsDarkThemeEnabled(true);
168+
theme = "dark";
169+
}
170+
setCookie(themeCookie, theme, getCookieOptions(serverURL));
171+
172+
const learningMFEUnitIframe = document.getElementById("unit-iframe");
173+
if (learningMFEUnitIframe) {
174+
learningMFEUnitIframe.contentWindow.postMessage(
175+
{ "indigo-toggle-dark": theme },
176+
serverURL.origin
177+
);
178+
}
179+
};
180+
181+
const hanldeKeyUp = (event) => {
182+
if (event.key === "Enter") {
183+
onToggleTheme();
184+
}
185+
};
186+
187+
if (!isThemeToggleEnabled) {
188+
return <div />;
189+
}
190+
191+
const messages = {
192+
"header.user.theme": {
193+
id: "header.user.theme",
194+
defaultMessage: "Toggle Theme",
195+
description: "Toggle between light and dark theme",
196+
},
197+
};
198+
199+
return (
200+
<div className="theme-toggle-button mr-3">
201+
<div className="light-theme-icon">
202+
<Icon src={WbSunny} />
203+
</div>
204+
<div className="toggle-switch">
205+
<label htmlFor="theme-toggle-checkbox" className="switch">
206+
<input
207+
id="theme-toggle-checkbox"
208+
defaultChecked={getCookie(themeCookie) === "dark"}
209+
onChange={onToggleTheme}
210+
onKeyUp={hanldeKeyUp}
211+
type="checkbox"
212+
title={intl.formatMessage(messages["header.user.theme"])}
213+
/>
214+
<span className="slider round" />
215+
<span id="theme-label" className="sr-only">{`Switch to ${
216+
isDarkThemeEnabled ? "Light" : "Dark"
217+
} Mode`}</span>
218+
</label>
219+
</div>
220+
<div className="dark-theme-icon">
221+
<Icon src={Nightlight} />
222+
</div>
223+
</div>
224+
);
225+
};
226+
227+
const MobileViewHeader = () => {
228+
const config = getConfig();
229+
const intl = useIntl();
230+
const messages = {
231+
"mobile.view.header.logo.altText": {
232+
id: "mobile.view.header.logo.altText",
233+
defaultMessage: "My Open edX",
234+
description: "Mobile view header logo altText",
235+
},
236+
};
237+
return (
238+
<div className="container-xl py-2 d-flex align-items-center justify-content-between">
239+
<a
240+
className="logo"
241+
href={`${config.LMS_BASE_URL}/dashboard`}
242+
style={Object.assign({}, { display: "flex", alignItems: "center" })}
243+
>
244+
<img
245+
className="d-block"
246+
src={`${config.LMS_BASE_URL}/theming/asset/images/logo.png`}
247+
alt={intl.formatMessage(messages["mobile.view.header.logo.altText"])}
248+
height={21}
249+
/>
250+
</a>
251+
<ToggleThemeButton />
252+
</div>
253+
);
254+
};

0 commit comments

Comments
 (0)