Skip to content

Commit

Permalink
feat: Implement Footer using UI Plugin Approach
Browse files Browse the repository at this point in the history
  • Loading branch information
hinakhadim committed Feb 20, 2024
1 parent 1aa6b3e commit c3466c4
Show file tree
Hide file tree
Showing 11 changed files with 621 additions and 2,159 deletions.
2,429 changes: 302 additions & 2,127 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@edly-io/indigo-frontend-component-footer",
"name": "@edx/frontend-component-footer",
"version": "1.0.0",
"description": "Footer component for use when building Open edX frontend applications",
"main": "dist/index.js",
Expand Down Expand Up @@ -56,7 +56,8 @@
"@fortawesome/free-brands-svg-icons": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/react-fontawesome": "0.2.0"
"@fortawesome/react-fontawesome": "0.2.0",
"@openedx-plugins/footer-links": "file:src/plugins/footer-links"
},
"peerDependencies": {
"@edx/frontend-platform": "^4.0.0 || ^5.0.0",
Expand Down
115 changes: 85 additions & 30 deletions src/components/Footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { getConfig } from '@edx/frontend-platform';

import messages from './Footer.messages';
import LanguageSelector from './LanguageSelector';
import UISlot, { DefaultUISlot, defaultRender, UiPluginsContext } from '../plugin-template';
import { getInjectedPlugins } from '../utils';

ensureConfig([
'LMS_BASE_URL',
Expand All @@ -35,6 +37,12 @@ class SiteFooter extends React.Component {
sendTrackEvent(eventName, properties);
}

async componentDidMount() {
console.log('============component did mount============');
const uploadedPlugin = await getInjectedPlugins();
console.log('-----------uploaded Plugin-----------', uploadedPlugin);
}

render() {
const {
supportedLanguages,
Expand All @@ -45,44 +53,91 @@ class SiteFooter extends React.Component {
const showLanguageSelector = supportedLanguages.length > 0 && onLanguageSelected;
const config = getConfig();

return (
<div className="wrapper wrapper-footer">
<footer id="footer" className="tutor-container">
<div className="footer-top">
<div className="powered-area">
<ul className="logo-list">
<li>{intl.formatMessage(messages['footer.poweredby.text'])}</li>
const FooterTopSection = (
<div className="footer-top">
<div className="powered-area">
<ul className="logo-list">
<DefaultUISlot slotId="powered-by-text"><li>{intl.formatMessage(messages['footer.poweredby.text'])}</li></DefaultUISlot>
<UISlot
slotId="logos"
defaultContents={[
{
id: 'tutor-logo',
priority: 1,
content: {
link: 'https://docs.tutor.overhang.io',
srcUrl: `${config.LMS_BASE_URL}/static/indigo/images/tutor-logo.png`,
altText: intl.formatMessage(messages['footer.tutorlogo.altText']),
width: 57,
},
},
{
id: 'openedx-logo',
priority: 50,
content: {
link: 'https://open.edx.org',
srcUrl: logo || `${config.LMS_BASE_URL}/static/indigo/images/openedx-logo.png`,
altText: intl.formatMessage(messages['footer.logo.altText']),
width: 79,
},
},
]}
renderWidget={content => (
<li>
<a href="https://docs.tutor.overhang.io" rel="noreferrer" target="_blank">
<a href={content.link} rel="noreferrer" target="_blank">
<Image
src={`${config.LMS_BASE_URL}/static/indigo/images/tutor-logo.png`}
alt={intl.formatMessage(messages['footer.tutorlogo.altText'])}
width="57"
src={content.srcUrl}
alt={content.altText}
width={content.width}
/>
</a>
</li>
<li>
<a href="https://open.edx.org" rel="noreferrer" target="_blank">
<Image
src={logo || `${config.LMS_BASE_URL}/static/indigo/images/openedx-logo.png`}
alt={intl.formatMessage(messages['footer.logo.altText'])}
width="79"
/>
</a>
</li>
</ul>
</div>
</div>
<span className="copyright-site">{intl.formatMessage(messages['footer.copyright.text'])}</span>
{showLanguageSelector && (
<LanguageSelector
options={supportedLanguages}
onSubmit={onLanguageSelected}
)}
/>
)}
</footer>
</ul>
</div>
</div>
);

const footerContents = [
{
id: 'footer-top',
priority: 1,
content: FooterTopSection,
},
{
id: 'copyright-site',
priority: 3,
content: <span className="copyright-site">{intl.formatMessage(messages['footer.copyright.text'])}</span>,
},
{
id: 'language-selector',
priority: 5,
content: <LanguageSelector
options={supportedLanguages}
onSubmit={onLanguageSelected}
/>,
hidden: showLanguageSelector,
},
];

return (
<UiPluginsContext.Provider value={[]}>
<div className="wrapper wrapper-footer">
<UISlot
slotId="footer"
defaultContents={[
{
id: 'footer-container',
priority: 1,
content: <UISlot slotId="footer-contents" defaultContents={footerContents} renderWidget={defaultRender} />,
}]}
renderWidget={(widget) => <footer id="footer" className="tutor-container">{widget.content}</footer>}
/>

</div>
</UiPluginsContext.Provider>
);
}
}

Expand Down
24 changes: 24 additions & 0 deletions src/plugin-template/DefaultUISlot.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import { UISlot } from './UISlot';

export const defaultRender = (widget) => (
<React.Fragment key={widget.id}>{widget.content}</React.Fragment>
);

export const DefaultUISlot = (props) => (
<UISlot
slotId={props.slotId}
renderWidget={defaultRender}
defaultContents={
props.children
? [{ id: 'content', priority: 1, content: props.children }]
: []
}
/>
);

DefaultUISlot.propTypes = {
slotId: PropTypes.string.isRequired,
children: PropTypes.element.isRequired,
};
85 changes: 85 additions & 0 deletions src/plugin-template/UISlot.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/* eslint-disable no-restricted-syntax */
import React, { createContext, useContext, useMemo } from 'react';
import PropTypes from 'prop-types';

const UiChangeOperation = {
INSERT: 'insert',
HIDE: 'hide',
MODIFY: 'modify',
WRAP: 'wrap',
};

export const UiPluginsContext = createContext([]);

export const UISlot = (props) => {
const enabledPlugins = useContext(UiPluginsContext);

const allContents = useMemo(() => {
const contents = [...(props.defaultContents ?? [])];

for (const p of enabledPlugins) {
const changes = p.getUiSlotChanges();
for (const change of changes[props.slotId] ?? []) {
if (change.op === UiChangeOperation.INSERT) {
contents.push(change.widget);
} else if (change.op === UiChangeOperation.HIDE) {
const widget = contents.find((w) => w.id === change.widgetId);
if (widget) {
widget.hidden = true;
}
} else if (change.op === UiChangeOperation.MODIFY) {
const widgetIdx = contents.findIndex((w) => w.id === change.widgetId);
if (widgetIdx >= 0) {
const widget = { ...contents[widgetIdx] };
contents[widgetIdx] = change.fn(widget);
}
} else if (change.op === UiChangeOperation.WRAP) {
const widgetIdx = contents.findIndex((w) => w.id === change.widgetId);
if (widgetIdx >= 0) {
const newWidget = { wrappers: [], ...contents[widgetIdx] };
newWidget.wrappers.push(change.wrapper);
contents[widgetIdx] = newWidget;
}
} else {
throw new Error(`unknown plugin UI change operation: ${change.op}`);
}
}
}

// Sort first by priority, then by ID
contents.sort(
(a, b) => (a.priority - b.priority) * 10_000 + a.id.localeCompare(b.id),
);
return contents;
}, [props.defaultContents, enabledPlugins, props.slotId]);

return (
<>
{allContents.map((c) => {
if (c.hidden) { return null; }

return (
c.wrappers
? c.wrappers.reduce(
(widget, wrapper) => React.createElement(wrapper, { widget, key: c.id }),
props.renderWidget(c),
)
: props.renderWidget(c));
})}
</>
);
};

UISlot.propTypes = {
slotId: PropTypes.string.isRequired,
defaultContents: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
priority: PropTypes.number,
content: PropTypes.node,
})),
renderWidget: PropTypes.element.isRequired,
};

UISlot.defaultProps = {
defaultContents: [],
};
5 changes: 5 additions & 0 deletions src/plugin-template/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { UISlot, UiPluginsContext } from './UISlot';

export { DefaultUISlot, defaultRender } from './DefaultUISlot';
export default UISlot;
export { UiPluginsContext };
44 changes: 44 additions & 0 deletions src/plugins/footer-links/FooterLinks.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import UISlot from '@edx/frontend-component-footer/src/plugin-template';
import useFooterLinks from './fetchFooterLinks';

const FooterLinks = () => {
const { data, loading } = useFooterLinks();

console.log('------data----', data);

if (loading) { return (<div>Loading...</div>); }

return (
<div className="footer-links-container">
<UISlot
slotId="footer-links"
defaultContents={[
{
id: 'footer-navigation-links',
priority: 1,
content: { className: 'navigation-links', links: data.navigation_links },
},
{
id: 'footer-legal-links',
priority: 5,
content: { className: 'legal-links', links: data.legal_links },
},
]}
renderWidget={(widget) => (
<div className={widget.content.className}>
<ul className="nav-list">
{widget.content.links?.map((link) => (
<li className="nav-item">
<a className="nav-link" href={link.url}>{link.title}</a>
</li>
))}
</ul>
</div>
)}
/>
</div>
);
};

export default FooterLinks;
25 changes: 25 additions & 0 deletions src/plugins/footer-links/fetchFooterLinks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect, useState } from 'react';
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getHttpClient } from '@edx/frontend-platform/auth';

const useFooterLinks = () => {
const [data, setData] = useState({});
const [loading, setLoading] = useState(false);

useEffect(() => {
const fetchData = async () => {
const url = `${getConfig().LMS_BASE_URL}/api/branding/v1/footer`;
setLoading(true);
const { data: footerLinks } = await getHttpClient().get(url);
const camelCasedData = camelCaseObject(footerLinks);
setData(camelCasedData);
setLoading(false);
};

fetchData();
}, []);

return { data, loading };
};

export default useFooterLinks;
21 changes: 21 additions & 0 deletions src/plugins/footer-links/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import FooterLinks from './FooterLinks';

const FooterLinksPlugin = {
id: 'footer-links',
getUiSlotChanges() {
return {
footer: [
{
op: 'insert',
widget: {
id: 'footer-links',
priority: 2,
content: <FooterLinks />,
},
},
],
};
},
};

export default FooterLinksPlugin;
17 changes: 17 additions & 0 deletions src/plugins/footer-links/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@openedx-plugins/footer-links",
"version": "0.1.0",
"description": "Footer Links configuration",
"peerDependencies": {
"@edly-io/indigo-frontend-component-footer": "*",
"@edx/frontend-platform": "*",
"@edx/paragon": "*",
"prop-types": "*",
"react": "*"
},
"peerDependenciesMeta": {
"@edly-io/indigo-frontend-component-footer": {
"optional": true
}
}
}
Loading

0 comments on commit c3466c4

Please sign in to comment.