diff --git a/.gitignore b/.gitignore
index 3fc643913a..83554b03e8 100755
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,7 @@
node_modules
npm-debug.log
coverage
-env.config.*
+# env.config.*
dist/
src/i18n/transifex_input.json
@@ -29,4 +29,4 @@ module.config.js
src/i18n/messages/
-env.config.jsx
+# env.config.jsx
diff --git a/env.config.jsx b/env.config.jsx
new file mode 100644
index 0000000000..eeadbc37bd
--- /dev/null
+++ b/env.config.jsx
@@ -0,0 +1,95 @@
+import React from 'react';
+import { AppContext } from '@edx/frontend-platform/react';
+import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
+import { Button } from '@openedx/paragon';
+import { useQuery } from '@tanstack/react-query'
+
+const config = {
+ pluginSlots: {
+ 'org.openedx.frontend.learning.unit_contents.v1': {
+ keepDefault: true,
+ plugins: [
+ {
+ // Display the unit ID *above* the content
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'before_unit_content',
+ priority: 10, // 10 will come before the unit content, which has priority 50
+ type: DIRECT_PLUGIN,
+ RenderWidget: (props) => (
+
+ This unit is {props.unitId}
+
+ ),
+ },
+ },
+ {
+ // Display the course ID *after* the content
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'after_unit_content',
+ priority: 80, // will come after the unit content, which has priority 50
+ type: DIRECT_PLUGIN,
+ RenderWidget: (props) => (
+
+ This course is {props.courseId}
+
+ ),
+ },
+ },
+ {
+ // Blur the content
+ op: PLUGIN_OPERATIONS.Wrap,
+ widgetId: 'default_contents', // Wrap the contents
+ wrapper: ({ component }) => {
+ const [isBlurred, setBlur] = React.useState(true);
+ const { authenticatedUser } = React.useContext(AppContext);
+ if (isBlurred) {
+ return (
+
+
+ {component}
+
+
+
{authenticatedUser?.username || 'Learner'}, are you sure you want to learn this now?
+
setBlur(false)}>Yes
+
+
+ );
+ } else {
+ return <>{component}>;
+ }
+ },
+ },
+ {
+ // Display a random dog picture after the each unit
+ op: PLUGIN_OPERATIONS.Insert,
+ widget: {
+ id: 'after_unit_dog',
+ priority: 90,
+ type: DIRECT_PLUGIN,
+ RenderWidget: (props) => {
+ const { data, isLoading, error } = useQuery({
+ queryKey: ['unit_dog', props.unitId],
+ queryFn: async () => {
+ const response = await fetch('https://dog.ceo/api/breeds/image/random');
+ return (await response.json()).message;
+ },
+ refetchOnWindowFocus: false,
+ refetchOnMount: false,
+ });
+ if (isLoading) return Loading doggo...
;
+ if (!data) return Error: {error}
;
+ return Bonus doggo for this unit:
;
+ },
+ },
+ },
+ ]
+ }
+ },
+}
+
+export default config;
diff --git a/package-lock.json b/package-lock.json
index 89ca93b040..7ba404eaa3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,7 @@
"@openedx/paragon": "^22.16.0",
"@popperjs/core": "2.11.8",
"@reduxjs/toolkit": "1.9.7",
+ "@tanstack/react-query": "^5.77.2",
"buffer": "^6.0.3",
"classnames": "2.5.1",
"copy-webpack-plugin": "^12.0.0",
@@ -5169,6 +5170,32 @@
"url": "https://github.com/sponsors/gregberge"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.77.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.77.2.tgz",
+ "integrity": "sha512-1lqJwPsR6GX6nZFw06erRt518O19tWU6Q+x0fJUygl4lxHCYF2nhzBPwLKk2NPjYOrpR0K567hxPc5K++xDe9Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.77.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.77.2.tgz",
+ "integrity": "sha512-BRHxWdy1mHmgAcYA/qy2IPLylT81oebLgkm9K85viN2Qol/Vq48t1dzDFeDIVQjTWDV96AmqsLNPlH5HjyKCxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.77.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
diff --git a/package.json b/package.json
index b4351459a1..0c192f2dda 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,7 @@
"@openedx/paragon": "^22.16.0",
"@popperjs/core": "2.11.8",
"@reduxjs/toolkit": "1.9.7",
+ "@tanstack/react-query": "^5.77.2",
"buffer": "^6.0.3",
"classnames": "2.5.1",
"copy-webpack-plugin": "^12.0.0",
diff --git a/src/courseware/course/sequence/Unit/index.jsx b/src/courseware/course/sequence/Unit/index.jsx
index 37eb396d88..a496946133 100644
--- a/src/courseware/course/sequence/Unit/index.jsx
+++ b/src/courseware/course/sequence/Unit/index.jsx
@@ -15,6 +15,7 @@ import { modelKeys, views } from './constants';
import { useExamAccess, useShouldDisplayHonorCode } from './hooks';
import { getIFrameUrl } from './urls';
import UnitTitleSlot from '../../../../plugin-slots/UnitTitleSlot';
+import UnitContentsSlot from '../../../../plugin-slots/UnitContentsSlot';
const Unit = ({
courseId,
@@ -50,16 +51,18 @@ const Unit = ({
-
+
+
+
);
};
diff --git a/src/index.jsx b/src/index.jsx
index 72c4992ac7..9df243298e 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -7,6 +7,7 @@ import { AppProvider, ErrorPage, PageWrap } from '@edx/frontend-platform/react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Routes, Route } from 'react-router-dom';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Helmet } from 'react-helmet';
import { fetchDiscussionTab, fetchLiveTab } from './course-home/data/thunks';
@@ -37,6 +38,8 @@ import { DECODE_ROUTES, ROUTES } from './constants';
import PreferencesUnsubscribe from './preferences-unsubscribe';
import PageNotFound from './generic/PageNotFound';
+const queryClient = new QueryClient();
+
subscribe(APP_READY, () => {
const root = createRoot(document.getElementById('root'));
@@ -49,7 +52,7 @@ subscribe(APP_READY, () => {
-
+
} />
} />
} />
@@ -57,7 +60,7 @@ subscribe(APP_READY, () => {
path={ROUTES.PREFERENCES_UNSUBSCRIBE}
element={
- }
+ }
/>
{
- )}
+ )}
/>
{
- )}
+ )}
/>
{
- )}
+ )}
/>
{
- )}
+ )}
/>
{DECODE_ROUTES.PROGRESS.map((route) => (
{
- )}
+ )}
/>
))}
{
- )}
+ )}
/>
{DECODE_ROUTES.COURSEWARE.map((route) => (
{
- )}
+ )}
/>
))}
-
+
diff --git a/src/plugin-slots/UnitContentsSlot/index.jsx b/src/plugin-slots/UnitContentsSlot/index.jsx
new file mode 100644
index 0000000000..15fc9f6e38
--- /dev/null
+++ b/src/plugin-slots/UnitContentsSlot/index.jsx
@@ -0,0 +1,19 @@
+import PropTypes from 'prop-types';
+import { PluginSlot } from '@openedx/frontend-plugin-framework';
+
+const UnitContentsSlot = ({ courseId, unitId, children }) => (
+
+ {children}
+
+);
+
+UnitContentsSlot.propTypes = {
+ courseId: PropTypes.string.isRequired,
+ unitId: PropTypes.string.isRequired,
+ children: PropTypes.element.isRequired,
+};
+
+export default UnitContentsSlot;