Skip to content

Commit

Permalink
feat: implented notification preference UI for cadence (#1013)
Browse files Browse the repository at this point in the history
* feat: implented notification preference UI for cadence

* refactor: refactored code

* refactor: refactored code

* refactor: clean code after adding email cadence

* refactor: refactored and restructured notificationPreferences page

* refactor: refactored and implemented mobile view

---------

Co-authored-by: Awais Ansari <[email protected]>
  • Loading branch information
ayesha-waris and awais-ansari authored Mar 25, 2024
1 parent 6cc50a9 commit a266e3d
Show file tree
Hide file tree
Showing 12 changed files with 354 additions and 152 deletions.
6 changes: 6 additions & 0 deletions src/hooks.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { breakpoints, useWindowSize } from '@openedx/paragon';

import {
IDLE_STATUS, LOADING_STATUS, SUCCESS_STATUS, FAILURE_STATUS,
Expand Down Expand Up @@ -66,3 +67,8 @@ export function useFeedbackWrapper() {
}
}, []);
}

export function useIsOnMobile() {
const windowSize = useWindowSize();
return windowSize.width <= breakpoints.small.minWidth;
}
40 changes: 40 additions & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,39 @@ $fa-font-path: "~font-awesome/fonts";
color: #707070;
}

.preference-app {
font-size: 18px;
font-weight: 700;
}

.column-padding{
padding-left: 32px;
padding-right: 32px;
}

.notification-course-title {
line-height: 28px;
font-weight: 700;
font-size: 18px;
}

.font-size-14{
font-size: 14px !important;
}

.cadence-button {
width: 134px;
height: 36px;
font-weight: 500;
}

.line-height-36{
line-height: 36px;
}

.h-4\.5 {
height: 36px;
}
}

.usabilla_live_button_container {
Expand All @@ -102,3 +130,15 @@ $fa-font-path: "~font-awesome/fonts";
transform: rotate(270deg) !important;
}
}

@media screen and (max-width: 425px) {
.column-padding{
padding-left: 16px;
padding-right: 16px;
}

.margin-bottom-32{
margin-bottom: 32px !important;
}
}

67 changes: 67 additions & 0 deletions src/notification-preferences/EmailCadences.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Dropdown, ModalPopup, Button, useToggle,
} from '@openedx/paragon';
import { ExpandLess, ExpandMore } from '@openedx/paragon/icons';
import messages from './messages';
import { EMAIL_CADENCE } from './data/constants';

const EmailCadences = ({
email, onToggle, emailCadence, notificationType,
}) => {
const intl = useIntl();
const [isOpen, open, close] = useToggle(false);
const [target, setTarget] = useState(null);

return (
<>
<Button
ref={setTarget}
variant="outline-primary"
onClick={open}
disabled={email}
size="sm"
iconAfter={isOpen ? ExpandLess : ExpandMore}
className="border-light-300 text-primary-500 justify-content-between ml-3.5 cadence-button"
>
{intl.formatMessage(messages.emailCadence, { text: emailCadence })}
</Button>
<ModalPopup
onClose={close}
positionRef={target}
isOpen={isOpen}
>
<div
className="bg-white shadow d-flex flex-column"
data-testid="email-cadence-dropdown"
>
{Object.values(EMAIL_CADENCE).map((cadence) => (
<Dropdown.Item
key={cadence}
name="email_cadence"
className="d-flex justify-content-start py-1.5"
as={Button}
variant="primary"
size="inline"
autoFocus={cadence === emailCadence}
onClick={(event) => onToggle(event, notificationType)}
>
{intl.formatMessage(messages.emailCadence, { text: cadence })}
</Dropdown.Item>
))}
</div>
</ModalPopup>
</>
);
};

EmailCadences.propTypes = {
email: PropTypes.bool.isRequired,
onToggle: PropTypes.func.isRequired,
emailCadence: PropTypes.oneOf(Object.values(EMAIL_CADENCE)).isRequired,
notificationType: PropTypes.string.isRequired,
};

export default React.memo(EmailCadences);
79 changes: 25 additions & 54 deletions src/notification-preferences/NotificationPreferenceApp.jsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,49 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Collapsible, NavItem } from '@openedx/paragon';
import { Collapsible } from '@openedx/paragon';
import classNames from 'classnames';
import messages from './messages';
import ToggleSwitch from './ToggleSwitch';
import {
selectPreferenceAppToggleValue,
selectNonEditablePreferences,
selectPreferencesOfApp,
selectSelectedCourseId,
selectUpdatePreferencesStatus,
} from './data/selectors';
import NotificationPreferenceRow from './NotificationPreferenceRow';
import { updateAppPreferenceToggle, updateChannelPreferenceToggle } from './data/thunks';
import NotificationPreferenceColumn from './NotificationPreferenceColumn';
import { updateAppPreferenceToggle } from './data/thunks';
import { LOADING_STATUS } from '../constants';
import NOTIFICATION_CHANNELS from './data/constants';
import { NOTIFICATION_CHANNELS } from './data/constants';
import NotificationTypes from './NotificationTypes';
import { useIsOnMobile } from '../hooks';

const NotificationPreferenceApp = ({ appId }) => {
const dispatch = useDispatch();
const intl = useIntl();
const courseId = useSelector(selectSelectedCourseId());
const appPreferences = useSelector(selectPreferencesOfApp(appId));
const appToggle = useSelector(selectPreferenceAppToggleValue(appId));
const updatePreferencesStatus = useSelector(selectUpdatePreferencesStatus());
const nonEditable = useSelector(selectNonEditablePreferences(appId));

const onChannelToggle = useCallback((event) => {
const { id: notificationChannel } = event.target;
const isPreferenceNonEditable = (preference) => nonEditable?.[preference.id]?.includes(notificationChannel);

const hasActivePreferences = appPreferences.some(
(preference) => preference[notificationChannel] && !isPreferenceNonEditable(preference),
);

dispatch(updateChannelPreferenceToggle(courseId, appId, notificationChannel, !hasActivePreferences));
}, [appId, appPreferences, courseId, dispatch, nonEditable]);

const preferences = useMemo(() => (
appPreferences.map(preference => (
<NotificationPreferenceRow
key={preference.id}
appId={appId}
preferenceName={preference.id}
/>
))), [appId, appPreferences]);
const mobileView = useIsOnMobile();

const onChangeAppSettings = useCallback((event) => {
dispatch(updateAppPreferenceToggle(courseId, appId, event.target.checked));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appId]);

if (!courseId) {
return null;
}

return (
<Collapsible.Advanced open={appToggle} data-testid={`${appId}-app`} className="mb-5">
<Collapsible.Advanced
open={appToggle}
data-testid={`${appId}-app`}
className={classNames({ 'mb-5': !mobileView && appToggle })}
>
<Collapsible.Trigger>
<div className="d-flex align-items-center">
<span className="mr-auto">
<span className="mr-auto preference-app">
{intl.formatMessage(messages.notificationAppTitle, { key: appId })}
</span>
<span className="d-flex" id={`${appId}-app-toggle`}>
Expand All @@ -71,33 +55,20 @@ const NotificationPreferenceApp = ({ appId }) => {
/>
</span>
</div>
<hr className="border-light-400 my-3" />
{!mobileView && <hr className="border-light-400 my-4" />}
</Collapsible.Trigger>
<Collapsible.Body>
<div className="d-flex flex-row header-label">
<span className="col-8 px-0">{intl.formatMessage(messages.typeLabel)}</span>
<span className="d-flex col-4 px-0">
{NOTIFICATION_CHANNELS.map((channel) => (
<NavItem
id={channel}
key={channel}
className={classNames(
'd-flex',
{ 'ml-auto': channel === 'web' },
{ 'mx-auto': channel === 'email' },
{ 'ml-auto mr-0': channel === 'push' },
)}
role="button"
onClick={onChannelToggle}
>
{intl.formatMessage(messages.notificationChannel, { text: channel })}
</NavItem>
<div className="d-flex flex-row justify-content-between">
<NotificationTypes appId={appId} />
{!mobileView && (
<div className="d-flex">
{Object.values(NOTIFICATION_CHANNELS).map((channel) => (
<NotificationPreferenceColumn key={channel} appId={appId} channel={channel} />
))}
</span>
</div>
<div className="my-3">
{ preferences }
</div>
)}
</div>
{mobileView && <hr className="border-light-400 my-4.5" />}
</Collapsible.Body>
</Collapsible.Advanced>
);
Expand Down
124 changes: 124 additions & 0 deletions src/notification-preferences/NotificationPreferenceColumn.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { NavItem } from '@openedx/paragon';
import messages from './messages';
import ToggleSwitch from './ToggleSwitch';
import {
selectNonEditablePreferences,
selectSelectedCourseId,
selectUpdatePreferencesStatus,
selectPreferencesOfApp,
} from './data/selectors';
import { updatePreferenceToggle, updateChannelPreferenceToggle } from './data/thunks';
import { LOADING_STATUS } from '../constants';
import EmailCadences from './EmailCadences';
import { useIsOnMobile } from '../hooks';

const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
const dispatch = useDispatch();
const intl = useIntl();
const courseId = useSelector(selectSelectedCourseId());
const appPreferences = useSelector(selectPreferencesOfApp(appId));
const nonEditable = useSelector(selectNonEditablePreferences(appId));
const updatePreferencesStatus = useSelector(selectUpdatePreferencesStatus());
const mobileView = useIsOnMobile();

const onChannelToggle = useCallback((event) => {
const { id: notificationChannel } = event.target;
const isPreferenceNonEditable = (preference) => nonEditable?.[preference.id]?.includes(notificationChannel);

const hasActivePreferences = appPreferences.some(
(preference) => preference[notificationChannel] && !isPreferenceNonEditable(preference),
);

dispatch(updateChannelPreferenceToggle(courseId, appId, notificationChannel, !hasActivePreferences));
}, [appId, appPreferences, courseId, dispatch, nonEditable]);

const onToggle = useCallback((event, notificationType) => {
const { name: notificationChannel } = event.target;
const value = notificationChannel === 'email_cadence' ? event.target.innerText : event.target.checked;

dispatch(updatePreferenceToggle(
courseId,
appId,
notificationType,
notificationChannel,
value,
));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appId]);

const renderPreference = (preference) => (
<div
key={`${preference.id}-${channel}`}
id={`${preference.id}-${channel}`}
className={classNames(
'd-flex align-items-center justify-content-center mb-2 h-4.5 column-padding',
{
'pr-0': channel === 'email',
'pl-0': channel === 'web' && mobileView,
},
)}
>
<ToggleSwitch
name={channel}
value={preference[channel]}
onChange={(event) => onToggle(event, preference.id)}
disabled={nonEditable?.[preference.id]?.includes(channel) || updatePreferencesStatus === LOADING_STATUS}
id={`${preference.id}-${channel}`}
className="my-1"
/>
{channel === 'email' && (
<EmailCadences
email={preference.email}
onToggle={onToggle}
emailCadence={preference.emailCadence}
notificationType={preference.id}
/>
)}
</div>
);

return (
<div className={classNames('d-flex flex-column', { 'border-right': channel !== 'email' })}>
<NavItem
id={channel}
key={channel}
role="button"
onClick={onChannelToggle}
className={classNames('mb-3 header-label column-padding', {
'pr-0': channel === 'email',
'pl-0': channel === 'web' && mobileView,
})}
>
{intl.formatMessage(messages.notificationChannel, { text: channel })}
</NavItem>
{appPreference
? renderPreference(appPreference)
: appPreferences.map((preference) => (renderPreference(preference)))}
</div>
);
};

NotificationPreferenceColumn.propTypes = {
appId: PropTypes.string.isRequired,
channel: PropTypes.string.isRequired,
appPreference: PropTypes.shape({
id: PropTypes.string,
emailCadence: PropTypes.string,
appId: PropTypes.string,
info: PropTypes.string,
email: PropTypes.bool,
push: PropTypes.bool,
web: PropTypes.bool,
}),
};

NotificationPreferenceColumn.defaultProps = {
appPreference: null,
};

export default React.memo(NotificationPreferenceColumn);
Loading

0 comments on commit a266e3d

Please sign in to comment.