Skip to content

Commit

Permalink
feat: added implementation for new sidebar
Browse files Browse the repository at this point in the history
  • Loading branch information
sundasnoreen12 committed Dec 27, 2023
1 parent 600f5b4 commit b1003b5
Show file tree
Hide file tree
Showing 25 changed files with 965 additions and 7 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ TWITTER_HASHTAG=''
TWITTER_URL=''
USER_INFO_COOKIE_NAME=''
OPTIMIZELY_FULL_STACK_SDK_KEY=''
ENABLE_SIDEBAR_NEW_VIEW='false'
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ SESSION_COOKIE_DOMAIN='localhost'
CHAT_RESPONSE_URL='http://localhost:18000/api/learning_assistant/v1/course_id'
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
OPTIMIZELY_FULL_STACK_SDK_KEY=''
ENABLE_SIDEBAR_NEW_VIEW='false'
11 changes: 8 additions & 3 deletions src/courseware/course/Course.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import ContentTools from './content-tools';
import CourseBreadcrumbs from './CourseBreadcrumbs';
import SidebarProvider from './sidebar/SidebarContextProvider';
import SidebarTriggers from './sidebar/SidebarTriggers';
import NewSidebarProvider from './new-sidebar/SidebarContextProvider';
import NewSidebarTriggers from './new-sidebar/SidebarTriggers';

import { useModel } from '../../generic/model-store';

Expand All @@ -34,6 +36,7 @@ const Course = ({
} = useModel('courseHomeMeta', courseId);
const sequence = useModel('sequences', sequenceId);
const section = useModel('sections', sequence ? sequence.sectionId : null);
const showSidebarNewView = getConfig().ENABLE_SIDEBAR_NEW_VIEW;

const pageTitleBreadCrumbs = [
sequence,
Expand Down Expand Up @@ -64,8 +67,10 @@ const Course = ({
));
}, [sequenceId]);

const SidebarProviderComponent = showSidebarNewView === 'true' ? NewSidebarProvider : SidebarProvider;

return (
<SidebarProvider courseId={courseId} unitId={unitId}>
<SidebarProviderComponent courseId={courseId} unitId={unitId}>
<Helmet>
<title>{`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`}</title>
</Helmet>
Expand All @@ -86,7 +91,7 @@ const Course = ({
courseId={courseId}
contentToolsEnabled={course.showCalculator || course.notes.enabled}
/>
<SidebarTriggers />
{ showSidebarNewView === 'true' ? <NewSidebarTriggers /> : <SidebarTriggers /> }
</>
)}
</div>
Expand All @@ -112,7 +117,7 @@ const Course = ({
onClose={() => setWeeklyGoalCelebrationOpen(false)}
/>
<ContentTools course={course} />
</SidebarProvider>
</SidebarProviderComponent>
);
};

Expand Down
41 changes: 41 additions & 0 deletions src/courseware/course/new-sidebar/NewSidebarIcon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Icon } from '@edx/paragon';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { useContext } from 'react';
import { RightSidebarFilled, RightSidebarOutlined } from './icons/index';
import SidebarContext from './SidebarContext';
import messages from '../messages';

const SidebarIcon = ({
intl,
status,
sidebarColor,
}) => {
const { currentSidebar } = useContext(SidebarContext);
return (
<>
<Icon src={currentSidebar ? RightSidebarFilled : RightSidebarOutlined} className="m-0 m-auto" alt={intl.formatMessage(messages.openNotificationTrigger)} />
{status === 'active'
? (
<span
className={classNames(sidebarColor, 'rounded-circle p-1 position-absolute')}
data-testid="notification-dot"
style={{
top: '0.3rem',
right: '0.55rem',
}}
/>
)
: null}
</>
);
};

SidebarIcon.propTypes = {
intl: intlShape.isRequired,
status: PropTypes.string.isRequired,
sidebarColor: PropTypes.string.isRequired,
};

export default injectIntl(SidebarIcon);
91 changes: 91 additions & 0 deletions src/courseware/course/new-sidebar/NewSidebarTrigger.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import React, { useContext, useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { getLocalStorage, setLocalStorage } from '../../../data/localStorage';
import { getSessionStorage, setSessionStorage } from '../../../data/sessionStorage';
import messages from './messages';
import SidebarTriggerBase from './common/TriggerBase';
import SidebarContext from './SidebarContext';
import { useModel } from '../../../generic/model-store';
import { getCourseDiscussionTopics } from '../../data/thunks';
import NewSidebarIcon from './NewSidebarIcon';

const NewSideBarTrigger = ({
intl,
onClick,
}) => {
const {
courseId,
notificationStatus,
setNotificationStatus,
upgradeNotificationCurrentState,
isNotificationbarAvailable,
isDiscussionbarAvailable,
} = useContext(SidebarContext);

const dispatch = useDispatch();
const { tabs } = useModel('courseHomeMeta', courseId);
const baseUrl = getConfig().DISCUSSIONS_MFE_BASE_URL;
const edxProvider = useMemo(
() => tabs?.find(tab => tab.slug === 'discussion'),
[tabs],
);

useEffect(() => {
if (baseUrl && edxProvider) {
dispatch(getCourseDiscussionTopics(courseId));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [courseId, baseUrl, edxProvider]);

/* Re-show a red dot beside the notification trigger for each of the 7 UpgradeNotification stages
The upgradeNotificationCurrentState prop will be available after UpgradeNotification mounts. Once available,
compare with the last state they've seen, and if it's different then set dot back to red */
function updateUpgradeNotificationLastSeen() {
if (upgradeNotificationCurrentState) {
if (getLocalStorage(`upgradeNotificationLastSeen.${courseId}`) !== upgradeNotificationCurrentState) {
setNotificationStatus('active');
setLocalStorage(`notificationStatus.${courseId}`, 'active');
setLocalStorage(`upgradeNotificationLastSeen.${courseId}`, upgradeNotificationCurrentState);
}
}
}

if (!getLocalStorage(`notificationStatus.${courseId}`)) {
setLocalStorage(`notificationStatus.${courseId}`, 'active'); // Show red dot on notificationTrigger until seen
}

if (!getLocalStorage(`upgradeNotificationCurrentState.${courseId}`)) {
setLocalStorage(`upgradeNotificationCurrentState.${courseId}`, 'initialize');
}

useEffect(() => {
updateUpgradeNotificationLastSeen();
});

const handleClick = () => {
if (getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open') {
setSessionStorage(`notificationTrayStatus.${courseId}`, 'closed');
} else {
setSessionStorage(`notificationTrayStatus.${courseId}`, 'open');
}
onClick();
};

if (!isDiscussionbarAvailable && !isNotificationbarAvailable) { return null; }

return (
<SidebarTriggerBase onClick={handleClick} ariaLabel={intl.formatMessage(messages.openSidebarTrigger)}>
<NewSidebarIcon status={notificationStatus} sidebarColor="bg-danger-500" />
</SidebarTriggerBase>
);
};

NewSideBarTrigger.propTypes = {
intl: intlShape.isRequired,
onClick: PropTypes.func.isRequired,
};

export default injectIntl(NewSideBarTrigger);
46 changes: 46 additions & 0 deletions src/courseware/course/new-sidebar/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useContext } from 'react';
import { ArrowBackIos } from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon } from '@edx/paragon';
import classNames from 'classnames';
import NotificationTray from './sidebars/notifications/NotificationTray';
import DiscussionsSidebar from './sidebars/discussions/DiscussionsSidebar';
import SidebarContext from './SidebarContext';
import messages from './messages';

const Sidebar = () => {
const intl = useIntl();

const {
toggleSidebar,
shouldDisplayFullScreen,
currentSidebar,
} = useContext(SidebarContext);

if (currentSidebar === null) { return null; }

return (
<div className={classNames('vh-100 d-flex flex-column', { 'bg-white fixed-top': shouldDisplayFullScreen })}>
{shouldDisplayFullScreen
&& (
<div
className="pt-2 pb-2.5 border-bottom border-light-400 d-flex align-items-center ml-2"
onClick={() => toggleSidebar(null)}
onKeyDown={() => toggleSidebar(null)}
role="button"
tabIndex="0"
alt={intl.formatMessage(messages.responsiveCloseSidebarTray)}
>
<Icon src={ArrowBackIos} />
<span className="font-weight-bold m-2 d-inline-block">
{intl.formatMessage(messages.responsiveCloseSidebarTray)}
</span>
</div>
)}
<NotificationTray />
<DiscussionsSidebar />
</div>
);
};

export default Sidebar;
5 changes: 5 additions & 0 deletions src/courseware/course/new-sidebar/SidebarContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';

const SidebarContext = React.createContext({});

export default SidebarContext;
121 changes: 121 additions & 0 deletions src/courseware/course/new-sidebar/SidebarContextProvider.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { breakpoints, useWindowSize } from '@edx/paragon';
import PropTypes from 'prop-types';
import React, {
useEffect, useState, useMemo, useCallback,
} from 'react';
import isEmpty from 'lodash/isEmpty';
import { SidebarID, Notifications, Discussions } from './constants';
import { getLocalStorage, setLocalStorage } from '../../../data/localStorage';
import SidebarContext from './SidebarContext';
import { useModel } from '../../../generic/model-store';

const SidebarProvider = ({
courseId,
unitId,
children,
}) => {
const shouldDisplayFullScreen = useWindowSize().width < breakpoints.large.minWidth;
const shouldDisplaySidebarOpen = useWindowSize().width > breakpoints.medium.minWidth;
const query = new URLSearchParams(window.location.search);
const initialSidebar = (shouldDisplaySidebarOpen || query.get('sidebar') === 'true') ? SidebarID : null;
const [currentSidebar, setCurrentSidebar] = useState(initialSidebar);
const [hideDiscussionbar, setHideDiscussionbar] = useState(false);
const [isDiscussionbarAvailable, setIsDiscussionbarAvailable] = useState(true);
const [hideNotificationbar, setHideNotificationbar] = useState(false);
const [isNotificationbarAvailable, setIsNotificationbarAvailable] = useState(true);

const [notificationStatus, setNotificationStatus] = useState(getLocalStorage(`notificationStatus.${courseId}`));
const [upgradeNotificationCurrentState, setUpgradeNotificationCurrentState] = useState(getLocalStorage(`upgradeNotificationCurrentState.${courseId}`));
const topic = useModel('discussionTopics', unitId);
const { verifiedMode } = useModel('courseHomeMeta', courseId);

useEffect(() => {
if (!topic?.id || !topic?.enabledInContext) {
setIsDiscussionbarAvailable(false);
setHideDiscussionbar(true);
} else {
setIsDiscussionbarAvailable(true);
setHideDiscussionbar(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [topic]);

useEffect(() => {
if (isEmpty(verifiedMode)) {
setIsNotificationbarAvailable(false);
setHideNotificationbar(true);
} else {
setIsNotificationbarAvailable(true);
setHideNotificationbar(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [verifiedMode]);

useEffect(() => {
setCurrentSidebar(SidebarID);
if (isDiscussionbarAvailable) { setHideDiscussionbar(false); } else { setHideDiscussionbar(true); }
if (isNotificationbarAvailable) { setHideNotificationbar(false); } else { setHideNotificationbar(true); }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [unitId, isDiscussionbarAvailable, isNotificationbarAvailable]);

const onNotificationSeen = useCallback(() => {
setNotificationStatus('inactive');
setLocalStorage(`notificationStatus.${courseId}`, 'inactive');
}, [courseId]);

useEffect(() => {
if (hideDiscussionbar && hideNotificationbar) {
setCurrentSidebar(null);
}
}, [hideDiscussionbar, hideNotificationbar]);

const toggleSidebar = useCallback((sidebarId, tabId) => {
if (tabId === Discussions) {
setHideDiscussionbar(true);
} else if (tabId === Notifications) {
setHideNotificationbar(true);
} else {
setCurrentSidebar(prevSidebar => (sidebarId === prevSidebar ? null : sidebarId));
if (isDiscussionbarAvailable) { setHideDiscussionbar(false); }
if (isNotificationbarAvailable) { setHideNotificationbar(false); }
}
}, [isNotificationbarAvailable, isDiscussionbarAvailable]);

const contextValue = useMemo(() => ({
toggleSidebar,
onNotificationSeen,
setNotificationStatus,
currentSidebar,
notificationStatus,
upgradeNotificationCurrentState,
setUpgradeNotificationCurrentState,
shouldDisplaySidebarOpen,
shouldDisplayFullScreen,
courseId,
unitId,
hideDiscussionbar,
hideNotificationbar,
isNotificationbarAvailable,
isDiscussionbarAvailable,
}), [courseId, currentSidebar, notificationStatus, onNotificationSeen, shouldDisplayFullScreen,
shouldDisplaySidebarOpen, toggleSidebar, unitId, upgradeNotificationCurrentState, hideDiscussionbar,
hideNotificationbar, isNotificationbarAvailable, isDiscussionbarAvailable]);

return (
<SidebarContext.Provider value={contextValue}>
{children}
</SidebarContext.Provider>
);
};

SidebarProvider.propTypes = {
courseId: PropTypes.string.isRequired,
unitId: PropTypes.string.isRequired,
children: PropTypes.node,
};

SidebarProvider.defaultProps = {
children: null,
};

export default SidebarProvider;
28 changes: 28 additions & 0 deletions src/courseware/course/new-sidebar/SidebarTriggers.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import classNames from 'classnames';
import React, { useContext } from 'react';
import SidebarContext from './SidebarContext';
import NewSidebarTrigger from './NewSidebarTrigger';
import { SidebarID } from './constants';

const SidebarTriggers = () => {
const {
toggleSidebar,
currentSidebar,
} = useContext(SidebarContext);
const isActive = currentSidebar === SidebarID;
return (
<div className="d-flex ml-auto">
<div
className={classNames('mt-3', { 'border-primary-700': isActive })}
style={{ borderBottom: isActive ? '2px solid' : null }}
key={SidebarID}
>
<NewSidebarTrigger onClick={() => toggleSidebar(SidebarID)} key={SidebarID} />
</div>
</div>
);
};

SidebarTriggers.propTypes = {};

export default SidebarTriggers;
Loading

0 comments on commit b1003b5

Please sign in to comment.