Skip to content

Commit c3466c4

Browse files
committed
feat: Implement Footer using UI Plugin Approach
1 parent 1aa6b3e commit c3466c4

File tree

11 files changed

+621
-2159
lines changed

11 files changed

+621
-2159
lines changed

package-lock.json

Lines changed: 302 additions & 2127 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@edly-io/indigo-frontend-component-footer",
2+
"name": "@edx/frontend-component-footer",
33
"version": "1.0.0",
44
"description": "Footer component for use when building Open edX frontend applications",
55
"main": "dist/index.js",
@@ -56,7 +56,8 @@
5656
"@fortawesome/free-brands-svg-icons": "6.4.2",
5757
"@fortawesome/free-regular-svg-icons": "6.4.2",
5858
"@fortawesome/free-solid-svg-icons": "6.4.2",
59-
"@fortawesome/react-fontawesome": "0.2.0"
59+
"@fortawesome/react-fontawesome": "0.2.0",
60+
"@openedx-plugins/footer-links": "file:src/plugins/footer-links"
6061
},
6162
"peerDependencies": {
6263
"@edx/frontend-platform": "^4.0.0 || ^5.0.0",

src/components/Footer.jsx

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { getConfig } from '@edx/frontend-platform';
99

1010
import messages from './Footer.messages';
1111
import LanguageSelector from './LanguageSelector';
12+
import UISlot, { DefaultUISlot, defaultRender, UiPluginsContext } from '../plugin-template';
13+
import { getInjectedPlugins } from '../utils';
1214

1315
ensureConfig([
1416
'LMS_BASE_URL',
@@ -35,6 +37,12 @@ class SiteFooter extends React.Component {
3537
sendTrackEvent(eventName, properties);
3638
}
3739

40+
async componentDidMount() {
41+
console.log('============component did mount============');
42+
const uploadedPlugin = await getInjectedPlugins();
43+
console.log('-----------uploaded Plugin-----------', uploadedPlugin);
44+
}
45+
3846
render() {
3947
const {
4048
supportedLanguages,
@@ -45,44 +53,91 @@ class SiteFooter extends React.Component {
4553
const showLanguageSelector = supportedLanguages.length > 0 && onLanguageSelected;
4654
const config = getConfig();
4755

48-
return (
49-
<div className="wrapper wrapper-footer">
50-
<footer id="footer" className="tutor-container">
51-
<div className="footer-top">
52-
<div className="powered-area">
53-
<ul className="logo-list">
54-
<li>{intl.formatMessage(messages['footer.poweredby.text'])}</li>
56+
const FooterTopSection = (
57+
<div className="footer-top">
58+
<div className="powered-area">
59+
<ul className="logo-list">
60+
<DefaultUISlot slotId="powered-by-text"><li>{intl.formatMessage(messages['footer.poweredby.text'])}</li></DefaultUISlot>
61+
<UISlot
62+
slotId="logos"
63+
defaultContents={[
64+
{
65+
id: 'tutor-logo',
66+
priority: 1,
67+
content: {
68+
link: 'https://docs.tutor.overhang.io',
69+
srcUrl: `${config.LMS_BASE_URL}/static/indigo/images/tutor-logo.png`,
70+
altText: intl.formatMessage(messages['footer.tutorlogo.altText']),
71+
width: 57,
72+
},
73+
},
74+
{
75+
id: 'openedx-logo',
76+
priority: 50,
77+
content: {
78+
link: 'https://open.edx.org',
79+
srcUrl: logo || `${config.LMS_BASE_URL}/static/indigo/images/openedx-logo.png`,
80+
altText: intl.formatMessage(messages['footer.logo.altText']),
81+
width: 79,
82+
},
83+
},
84+
]}
85+
renderWidget={content => (
5586
<li>
56-
<a href="https://docs.tutor.overhang.io" rel="noreferrer" target="_blank">
87+
<a href={content.link} rel="noreferrer" target="_blank">
5788
<Image
58-
src={`${config.LMS_BASE_URL}/static/indigo/images/tutor-logo.png`}
59-
alt={intl.formatMessage(messages['footer.tutorlogo.altText'])}
60-
width="57"
89+
src={content.srcUrl}
90+
alt={content.altText}
91+
width={content.width}
6192
/>
6293
</a>
6394
</li>
64-
<li>
65-
<a href="https://open.edx.org" rel="noreferrer" target="_blank">
66-
<Image
67-
src={logo || `${config.LMS_BASE_URL}/static/indigo/images/openedx-logo.png`}
68-
alt={intl.formatMessage(messages['footer.logo.altText'])}
69-
width="79"
70-
/>
71-
</a>
72-
</li>
73-
</ul>
74-
</div>
75-
</div>
76-
<span className="copyright-site">{intl.formatMessage(messages['footer.copyright.text'])}</span>
77-
{showLanguageSelector && (
78-
<LanguageSelector
79-
options={supportedLanguages}
80-
onSubmit={onLanguageSelected}
95+
)}
8196
/>
82-
)}
83-
</footer>
97+
</ul>
98+
</div>
8499
</div>
85100
);
101+
102+
const footerContents = [
103+
{
104+
id: 'footer-top',
105+
priority: 1,
106+
content: FooterTopSection,
107+
},
108+
{
109+
id: 'copyright-site',
110+
priority: 3,
111+
content: <span className="copyright-site">{intl.formatMessage(messages['footer.copyright.text'])}</span>,
112+
},
113+
{
114+
id: 'language-selector',
115+
priority: 5,
116+
content: <LanguageSelector
117+
options={supportedLanguages}
118+
onSubmit={onLanguageSelected}
119+
/>,
120+
hidden: showLanguageSelector,
121+
},
122+
];
123+
124+
return (
125+
<UiPluginsContext.Provider value={[]}>
126+
<div className="wrapper wrapper-footer">
127+
<UISlot
128+
slotId="footer"
129+
defaultContents={[
130+
{
131+
id: 'footer-container',
132+
priority: 1,
133+
content: <UISlot slotId="footer-contents" defaultContents={footerContents} renderWidget={defaultRender} />,
134+
}]}
135+
renderWidget={(widget) => <footer id="footer" className="tutor-container">{widget.content}</footer>}
136+
/>
137+
138+
</div>
139+
</UiPluginsContext.Provider>
140+
);
86141
}
87142
}
88143

src/plugin-template/DefaultUISlot.jsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { UISlot } from './UISlot';
4+
5+
export const defaultRender = (widget) => (
6+
<React.Fragment key={widget.id}>{widget.content}</React.Fragment>
7+
);
8+
9+
export const DefaultUISlot = (props) => (
10+
<UISlot
11+
slotId={props.slotId}
12+
renderWidget={defaultRender}
13+
defaultContents={
14+
props.children
15+
? [{ id: 'content', priority: 1, content: props.children }]
16+
: []
17+
}
18+
/>
19+
);
20+
21+
DefaultUISlot.propTypes = {
22+
slotId: PropTypes.string.isRequired,
23+
children: PropTypes.element.isRequired,
24+
};

src/plugin-template/UISlot.jsx

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/* eslint-disable no-restricted-syntax */
2+
import React, { createContext, useContext, useMemo } from 'react';
3+
import PropTypes from 'prop-types';
4+
5+
const UiChangeOperation = {
6+
INSERT: 'insert',
7+
HIDE: 'hide',
8+
MODIFY: 'modify',
9+
WRAP: 'wrap',
10+
};
11+
12+
export const UiPluginsContext = createContext([]);
13+
14+
export const UISlot = (props) => {
15+
const enabledPlugins = useContext(UiPluginsContext);
16+
17+
const allContents = useMemo(() => {
18+
const contents = [...(props.defaultContents ?? [])];
19+
20+
for (const p of enabledPlugins) {
21+
const changes = p.getUiSlotChanges();
22+
for (const change of changes[props.slotId] ?? []) {
23+
if (change.op === UiChangeOperation.INSERT) {
24+
contents.push(change.widget);
25+
} else if (change.op === UiChangeOperation.HIDE) {
26+
const widget = contents.find((w) => w.id === change.widgetId);
27+
if (widget) {
28+
widget.hidden = true;
29+
}
30+
} else if (change.op === UiChangeOperation.MODIFY) {
31+
const widgetIdx = contents.findIndex((w) => w.id === change.widgetId);
32+
if (widgetIdx >= 0) {
33+
const widget = { ...contents[widgetIdx] };
34+
contents[widgetIdx] = change.fn(widget);
35+
}
36+
} else if (change.op === UiChangeOperation.WRAP) {
37+
const widgetIdx = contents.findIndex((w) => w.id === change.widgetId);
38+
if (widgetIdx >= 0) {
39+
const newWidget = { wrappers: [], ...contents[widgetIdx] };
40+
newWidget.wrappers.push(change.wrapper);
41+
contents[widgetIdx] = newWidget;
42+
}
43+
} else {
44+
throw new Error(`unknown plugin UI change operation: ${change.op}`);
45+
}
46+
}
47+
}
48+
49+
// Sort first by priority, then by ID
50+
contents.sort(
51+
(a, b) => (a.priority - b.priority) * 10_000 + a.id.localeCompare(b.id),
52+
);
53+
return contents;
54+
}, [props.defaultContents, enabledPlugins, props.slotId]);
55+
56+
return (
57+
<>
58+
{allContents.map((c) => {
59+
if (c.hidden) { return null; }
60+
61+
return (
62+
c.wrappers
63+
? c.wrappers.reduce(
64+
(widget, wrapper) => React.createElement(wrapper, { widget, key: c.id }),
65+
props.renderWidget(c),
66+
)
67+
: props.renderWidget(c));
68+
})}
69+
</>
70+
);
71+
};
72+
73+
UISlot.propTypes = {
74+
slotId: PropTypes.string.isRequired,
75+
defaultContents: PropTypes.arrayOf(PropTypes.shape({
76+
id: PropTypes.string,
77+
priority: PropTypes.number,
78+
content: PropTypes.node,
79+
})),
80+
renderWidget: PropTypes.element.isRequired,
81+
};
82+
83+
UISlot.defaultProps = {
84+
defaultContents: [],
85+
};

src/plugin-template/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { UISlot, UiPluginsContext } from './UISlot';
2+
3+
export { DefaultUISlot, defaultRender } from './DefaultUISlot';
4+
export default UISlot;
5+
export { UiPluginsContext };
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
import UISlot from '@edx/frontend-component-footer/src/plugin-template';
3+
import useFooterLinks from './fetchFooterLinks';
4+
5+
const FooterLinks = () => {
6+
const { data, loading } = useFooterLinks();
7+
8+
console.log('------data----', data);
9+
10+
if (loading) { return (<div>Loading...</div>); }
11+
12+
return (
13+
<div className="footer-links-container">
14+
<UISlot
15+
slotId="footer-links"
16+
defaultContents={[
17+
{
18+
id: 'footer-navigation-links',
19+
priority: 1,
20+
content: { className: 'navigation-links', links: data.navigation_links },
21+
},
22+
{
23+
id: 'footer-legal-links',
24+
priority: 5,
25+
content: { className: 'legal-links', links: data.legal_links },
26+
},
27+
]}
28+
renderWidget={(widget) => (
29+
<div className={widget.content.className}>
30+
<ul className="nav-list">
31+
{widget.content.links?.map((link) => (
32+
<li className="nav-item">
33+
<a className="nav-link" href={link.url}>{link.title}</a>
34+
</li>
35+
))}
36+
</ul>
37+
</div>
38+
)}
39+
/>
40+
</div>
41+
);
42+
};
43+
44+
export default FooterLinks;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useEffect, useState } from 'react';
2+
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
3+
import { getHttpClient } from '@edx/frontend-platform/auth';
4+
5+
const useFooterLinks = () => {
6+
const [data, setData] = useState({});
7+
const [loading, setLoading] = useState(false);
8+
9+
useEffect(() => {
10+
const fetchData = async () => {
11+
const url = `${getConfig().LMS_BASE_URL}/api/branding/v1/footer`;
12+
setLoading(true);
13+
const { data: footerLinks } = await getHttpClient().get(url);
14+
const camelCasedData = camelCaseObject(footerLinks);
15+
setData(camelCasedData);
16+
setLoading(false);
17+
};
18+
19+
fetchData();
20+
}, []);
21+
22+
return { data, loading };
23+
};
24+
25+
export default useFooterLinks;

src/plugins/footer-links/index.jsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import FooterLinks from './FooterLinks';
2+
3+
const FooterLinksPlugin = {
4+
id: 'footer-links',
5+
getUiSlotChanges() {
6+
return {
7+
footer: [
8+
{
9+
op: 'insert',
10+
widget: {
11+
id: 'footer-links',
12+
priority: 2,
13+
content: <FooterLinks />,
14+
},
15+
},
16+
],
17+
};
18+
},
19+
};
20+
21+
export default FooterLinksPlugin;

src/plugins/footer-links/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "@openedx-plugins/footer-links",
3+
"version": "0.1.0",
4+
"description": "Footer Links configuration",
5+
"peerDependencies": {
6+
"@edly-io/indigo-frontend-component-footer": "*",
7+
"@edx/frontend-platform": "*",
8+
"@edx/paragon": "*",
9+
"prop-types": "*",
10+
"react": "*"
11+
},
12+
"peerDependenciesMeta": {
13+
"@edly-io/indigo-frontend-component-footer": {
14+
"optional": true
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)