Skip to content

Frontend Plugin Workshop 2025 #1717

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
node_modules
npm-debug.log
coverage
env.config.*
# env.config.*

dist/
src/i18n/transifex_input.json
Expand All @@ -29,4 +29,4 @@ module.config.js

src/i18n/messages/

env.config.jsx
# env.config.jsx
95 changes: 95 additions & 0 deletions env.config.jsx
Original file line number Diff line number Diff line change
@@ -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) => (
<div style={{ backgroundColor: 'cornflowerblue', color: 'white', padding: '8px 2px' }}>
<small>This unit is <code style={{ color: 'inherit' }}>{props.unitId}</code></small>
</div>
),
},
},
{
// 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) => (
<div style={{ backgroundColor: 'lightcoral', color: 'white', padding: '8px 2px', margin: '8px 0' }}>
<small>This course is <code style={{ color: 'inherit' }}>{props.courseId}</code></small>
</div>
),
},
},
{
// 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 (
<div style={{ position: 'relative' }}>
<div style={{ filter: 'blur(5px)', pointerEvents: 'none' }}>
{component}
</div>
<div style={{
position: 'absolute', backgroundColor: 'white', left: '10%',
width: '80%', top: '200px', padding: '30px', border: '1px solid darkgrey',
}}>
<p>{authenticatedUser?.username || 'Learner'}, are you sure you want to learn this now?</p>
<Button onClick={() => setBlur(false)}>Yes</Button>
</div>
</div>
);
} 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 <div>Loading doggo...</div>;
if (!data) return <div>Error: {error}</div>;
return <div><p>Bonus doggo for this unit:</p><img src={data} alt="Doggo" /><br /><br /></div>;
},
},
},
]
}
},
}

export default config;
27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 13 additions & 10 deletions src/courseware/course/sequence/Unit/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -50,16 +51,18 @@ const Unit = ({
<div className="unit">
<UnitTitleSlot unitId={id} {...{ unit, isEnabledOutlineSidebar, renderUnitNavigation }} />
<UnitSuspense {...{ courseId, id }} />
<ContentIFrame
elementId="unit-iframe"
id={id}
iframeUrl={iframeUrl}
loadingMessage={formatMessage(messages.loadingSequence)}
onLoaded={onLoaded}
shouldShowContent={!shouldDisplayHonorCode && !examAccess.blockAccess}
title={unit.title}
courseId={courseId}
/>
<UnitContentsSlot courseId={courseId} unitId={id}>
<ContentIFrame
elementId="unit-iframe"
id={id}
iframeUrl={iframeUrl}
loadingMessage={formatMessage(messages.loadingSequence)}
onLoaded={onLoaded}
shouldShowContent={!shouldDisplayHonorCode && !examAccess.blockAccess}
title={unit.title}
courseId={courseId}
/>
</UnitContentsSlot>
</div>
);
};
Expand Down
23 changes: 13 additions & 10 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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';
Expand Down Expand Up @@ -37,6 +38,8 @@
import PreferencesUnsubscribe from './preferences-unsubscribe';
import PageNotFound from './generic/PageNotFound';

const queryClient = new QueryClient();

subscribe(APP_READY, () => {
const root = createRoot(document.getElementById('root'));

Expand All @@ -49,15 +52,15 @@
<PathFixesProvider>
<NoticesProvider>
<UserMessagesProvider>
<Routes>
<QueryClientProvider client={queryClient}><Routes>
<Route path="*" element={<PageWrap><PageNotFound /></PageWrap>} />
<Route path={ROUTES.UNSUBSCRIBE} element={<PageWrap><GoalUnsubscribe /></PageWrap>} />
<Route path={ROUTES.REDIRECT} element={<PageWrap><CoursewareRedirectLandingPage /></PageWrap>} />
<Route
path={ROUTES.PREFERENCES_UNSUBSCRIBE}
element={
<PageWrap><PreferencesUnsubscribe /></PageWrap>
}
}
/>
<Route
path={DECODE_ROUTES.ACCESS_DENIED}
Expand All @@ -71,7 +74,7 @@
<OutlineTab />
</TabContainer>
</DecodePageRoute>
)}
)}
/>
<Route
path={DECODE_ROUTES.LIVE}
Expand All @@ -81,7 +84,7 @@
<LiveTab />
</TabContainer>
</DecodePageRoute>
)}
)}
/>
<Route
path={DECODE_ROUTES.DATES}
Expand All @@ -91,7 +94,7 @@
<DatesTab />
</TabContainer>
</DecodePageRoute>
)}
)}
/>
<Route
path={DECODE_ROUTES.DISCUSSION}
Expand All @@ -101,7 +104,7 @@
<DiscussionTab />
</TabContainer>
</DecodePageRoute>
)}
)}
/>
{DECODE_ROUTES.PROGRESS.map((route) => (
<Route
Expand All @@ -118,7 +121,7 @@
<ProgressTab />
</TabContainer>
</DecodePageRoute>
)}
)}
/>
))}
<Route
Expand All @@ -129,7 +132,7 @@
<CourseExit />
</TabContainer>
</DecodePageRoute>
)}
)}
/>
{DECODE_ROUTES.COURSEWARE.map((route) => (
<Route
Expand All @@ -139,10 +142,10 @@
<DecodePageRoute>
<CoursewareContainer />
</DecodePageRoute>
)}
)}
/>
))}
</Routes>
</Routes></QueryClientProvider>

Check failure on line 148 in src/index.jsx

View workflow job for this annotation

GitHub Actions / tests

Closing tag of a multiline JSX expression must be on its own line

Check failure on line 148 in src/index.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected closing tag to match indentation of opening
</UserMessagesProvider>
</NoticesProvider>
</PathFixesProvider>
Expand Down
19 changes: 19 additions & 0 deletions src/plugin-slots/UnitContentsSlot/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import PropTypes from 'prop-types';
import { PluginSlot } from '@openedx/frontend-plugin-framework';

const UnitContentsSlot = ({ courseId, unitId, children }) => (
<PluginSlot
id="org.openedx.frontend.learning.unit_contents.v1"
pluginProps={{ courseId, unitId }}
>
{children}
</PluginSlot>
);

UnitContentsSlot.propTypes = {
courseId: PropTypes.string.isRequired,
unitId: PropTypes.string.isRequired,
children: PropTypes.element.isRequired,
};

export default UnitContentsSlot;
Loading