-
Notifications
You must be signed in to change notification settings - Fork 219
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added implementation for new sidebar
- Loading branch information
1 parent
600f5b4
commit b1003b5
Showing
25 changed files
with
965 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
121
src/courseware/course/new-sidebar/SidebarContextProvider.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.