feat: responsive behavior
This commit is contained in:
@@ -5,6 +5,12 @@
|
||||
.pgn__card-image-cap {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
.pgn__card-header-content {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.course-card-content-vertical {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.course-card-banners {
|
||||
@@ -13,4 +19,4 @@
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCard component snapshot 1`] = `
|
||||
exports[`CourseCard component snapshot: collapsed 1`] = `
|
||||
<div
|
||||
className="mb-4.5 course-card"
|
||||
data-testid="CourseCard"
|
||||
>
|
||||
<Card
|
||||
orientation="vertical"
|
||||
>
|
||||
<div
|
||||
className="d-flex flex-column w-100"
|
||||
>
|
||||
<CourseCardContent
|
||||
cardId="test-card-id"
|
||||
orientation="vertical"
|
||||
/>
|
||||
<div
|
||||
className="course-card-banners"
|
||||
data-testid="CourseCardBanners"
|
||||
>
|
||||
<CourseCardBanners
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CourseCard component snapshot: not collapsed 1`] = `
|
||||
<div
|
||||
className="mb-4.5 course-card"
|
||||
data-testid="CourseCard"
|
||||
@@ -14,63 +42,16 @@ exports[`CourseCard component snapshot 1`] = `
|
||||
<div
|
||||
className="d-flex"
|
||||
>
|
||||
<Card.ImageCap
|
||||
src="hooks.bannerUrl"
|
||||
srcAlt={
|
||||
Object {
|
||||
"formatted": Object {
|
||||
"defaultMessage": "Course thumbnail",
|
||||
"description": "Course card banner alt-text",
|
||||
"id": "learner-dash.courseCard.bannerAlt",
|
||||
},
|
||||
}
|
||||
}
|
||||
<CourseCardContent
|
||||
cardId="test-card-id"
|
||||
orientation="horizontal"
|
||||
/>
|
||||
<Card.Body>
|
||||
<Card.Header
|
||||
actions={
|
||||
<CourseCardMenu
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<span
|
||||
data-testid="CourseCardTitle"
|
||||
>
|
||||
hooks.title
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<Card.Section>
|
||||
<CourseCardDetails
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</Card.Section>
|
||||
<Card.Footer
|
||||
orientation="vertical"
|
||||
textElement={
|
||||
<RelatedProgramsBadge
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CourseCardActions
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</Card.Footer>
|
||||
</Card.Body>
|
||||
</div>
|
||||
<div
|
||||
className="course-card-banners"
|
||||
data-testid="CourseCardBanners"
|
||||
>
|
||||
<CourseBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<EntitlementBanner
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
<CertificateBanner
|
||||
<CourseCardBanners
|
||||
cardId="test-card-id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as CourseBanner } from './CourseBanner';
|
||||
export { default as CertificateBanner } from './CertificateBanner';
|
||||
export { default as EntitlementBanner } from './EntitlementBanner';
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CourseCard Actions component does not render secondary button if null is returned for secondary props 1`] = `
|
||||
<div
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<Button
|
||||
@@ -11,11 +11,11 @@ exports[`CourseCard Actions component does not render secondary button if null i
|
||||
>
|
||||
primary-children
|
||||
</Button>
|
||||
</div>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`CourseCard Actions component loads primary and secondary button props from hook 1`] = `
|
||||
<div
|
||||
<ActionRow
|
||||
data-test-id="CourseCardActions"
|
||||
>
|
||||
<Button
|
||||
@@ -32,5 +32,5 @@ exports[`CourseCard Actions component loads primary and secondary button props f
|
||||
>
|
||||
primary-children
|
||||
</Button>
|
||||
</div>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { ActionRow, Button } from '@edx/paragon';
|
||||
|
||||
import useCardActionData from './hooks';
|
||||
|
||||
export const CourseCardActions = ({ cardId }) => {
|
||||
const { primary, secondary } = useCardActionData({ cardId });
|
||||
return (
|
||||
<div data-test-id="CourseCardActions">
|
||||
<ActionRow data-test-id="CourseCardActions">
|
||||
{(secondary !== null) && (
|
||||
<Button {...secondary} />
|
||||
)}
|
||||
<Button {...primary} />
|
||||
</div>
|
||||
</ActionRow>
|
||||
);
|
||||
};
|
||||
CourseCardActions.propTypes = {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import CourseBanner from './CourseBanner';
|
||||
import CertificateBanner from './CertificateBanner';
|
||||
import EntitlementBanner from './EntitlementBanner';
|
||||
|
||||
export const CourseCardBanners = ({ cardId }) => {
|
||||
const { isEnrolled } = appHooks.useCardEnrollmentData(cardId);
|
||||
return (
|
||||
<div className="course-card-banners" data-testid="CourseCardBanners">
|
||||
<CourseBanner cardId={cardId} />
|
||||
<EntitlementBanner cardId={cardId} />
|
||||
{isEnrolled && <CertificateBanner cardId={cardId} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
CourseCardBanners.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseCardBanners;
|
||||
62
src/containers/CourseCard/components/CourseCardContent.jsx
Normal file
62
src/containers/CourseCard/components/CourseCardContent.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// import PropTypes from 'prop-types';
|
||||
import { Card } from '@edx/paragon';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
import RelatedProgramsBadge from './RelatedProgramsBadge';
|
||||
import CourseCardMenu from './CourseCardMenu';
|
||||
|
||||
import messages from '../messages';
|
||||
import CourseCardActions from './CourseCardActions';
|
||||
import CourseCardDetails from './CourseCardDetails';
|
||||
|
||||
export const CourseCardContent = ({ cardId, orientation }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { title, bannerUrl } = appHooks.useCardCourseData(cardId);
|
||||
return (
|
||||
<>
|
||||
<Card.ImageCap
|
||||
src={bannerUrl}
|
||||
srcAlt={formatMessage(messages.bannerAlt)}
|
||||
/>
|
||||
<Card.Body>
|
||||
<Card.Header
|
||||
title={<span data-testid="CourseCardTitle">{title}</span>}
|
||||
actions={<CourseCardMenu cardId={cardId} />}
|
||||
/>
|
||||
<Card.Section className="pt-0">
|
||||
<CourseCardDetails cardId={cardId} />
|
||||
</Card.Section>
|
||||
{orientation === 'vertical'
|
||||
? (
|
||||
<>
|
||||
<RelatedProgramsBadge cardId={cardId} />
|
||||
<Card.Footer orientation="horizontal">
|
||||
<CourseCardActions cardId={cardId} />
|
||||
</Card.Footer>
|
||||
</>
|
||||
) : (
|
||||
<Card.Footer
|
||||
orientation="vertical"
|
||||
textElement={<RelatedProgramsBadge cardId={cardId} />}
|
||||
>
|
||||
<CourseCardActions cardId={cardId} />
|
||||
</Card.Footer>
|
||||
)}
|
||||
</Card.Body>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CourseCardContent.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
orientation: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
CourseCardContent.defaultProps = {};
|
||||
|
||||
export default CourseCardContent;
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
exports[`CourseCard Details component does not have change session button on regular course 1`] = `
|
||||
<span
|
||||
className="small"
|
||||
data-testid="CourseCardDetails"
|
||||
>
|
||||
provider-name
|
||||
@@ -13,6 +14,7 @@ exports[`CourseCard Details component does not have change session button on reg
|
||||
|
||||
exports[`CourseCard Details component has change session button on entitlement course 1`] = `
|
||||
<span
|
||||
className="small"
|
||||
data-testid="CourseCardDetails"
|
||||
>
|
||||
provider-name
|
||||
|
||||
@@ -22,7 +22,7 @@ export const CourseCardDetails = ({ cardId }) => {
|
||||
} = useCardDetailsData({ cardId, dispatch });
|
||||
|
||||
return (
|
||||
<span data-testid="CourseCardDetails">
|
||||
<span className="small" data-testid="CourseCardDetails">
|
||||
{providerName} • {courseNumber}
|
||||
{!(isEntitlement && !isFulfilled) && (
|
||||
<>
|
||||
|
||||
33
src/containers/CourseCard/components/CourseCardLayout.jsx
Normal file
33
src/containers/CourseCard/components/CourseCardLayout.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Card } from '@edx/paragon';
|
||||
|
||||
import { useIsCollapsed } from '../hooks';
|
||||
import CourseCardBanners from './CourseCardBanners';
|
||||
import CourseCardContent from './CourseCardContent';
|
||||
|
||||
export const CourseCardLayout = ({
|
||||
cardId,
|
||||
}) => {
|
||||
const isCollapsed = useIsCollapsed();
|
||||
return (
|
||||
<div className="mb-4.5 course-card" data-testid="CourseCard">
|
||||
<Card orientation={isCollapsed ? 'vertical' : 'horizontal'}>
|
||||
<div className="d-flex flex-column w-100">
|
||||
<div className="d-flex">
|
||||
<CourseCardContent cardId={cardId} />
|
||||
</div>
|
||||
<div className="course-card-banners" data-testid="CourseCardBanners">
|
||||
<CourseCardBanners cardId={cardId} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
CourseCardLayout.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CourseCardLayout;
|
||||
@@ -3,6 +3,7 @@
|
||||
exports[`RelatedProgramsBadge component snapshot: 3 programs 1`] = `
|
||||
<Fragment>
|
||||
<Button
|
||||
className="pr-0 mr-0"
|
||||
data-testid="RelatedProgramsBadge"
|
||||
onClick={[MockFunction useRelatedProgramsBadge.openModal]}
|
||||
size="sm"
|
||||
|
||||
@@ -20,6 +20,7 @@ export const RelatedProgramsBadge = ({ cardId }) => {
|
||||
<>
|
||||
<Button
|
||||
data-testid="RelatedProgramsBadge"
|
||||
className="pr-0 mr-0"
|
||||
variant="tertiary"
|
||||
size="sm"
|
||||
iconBefore={Program}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
|
||||
export const useIsCollapsed = () => {
|
||||
const { width } = useWindowSize();
|
||||
return width < breakpoints.small.maxWidth;
|
||||
};
|
||||
|
||||
export const useCardData = ({ cardId }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { title, bannerUrl } = appHooks.useCardCourseData(cardId);
|
||||
|
||||
@@ -4,68 +4,39 @@ import PropTypes from 'prop-types';
|
||||
// import PropTypes from 'prop-types';
|
||||
import { Card } from '@edx/paragon';
|
||||
|
||||
import useCardData from './hooks';
|
||||
|
||||
import RelatedProgramsBadge from './components/RelatedProgramsBadge';
|
||||
import CourseCardMenu from './components/CourseCardMenu';
|
||||
import {
|
||||
CourseBanner,
|
||||
CertificateBanner,
|
||||
EntitlementBanner,
|
||||
} from './components/Banners';
|
||||
import CourseCardActions from './components/CourseCardActions';
|
||||
import messages from './messages';
|
||||
import CourseCardDetails from './components/CourseCardDetails';
|
||||
import { useIsCollapsed } from './hooks';
|
||||
import CourseCardContent from './components/CourseCardContent';
|
||||
import CourseCardBanners from './components/CourseCardBanners';
|
||||
|
||||
import './CourseCard.scss';
|
||||
|
||||
export const CourseCard = ({ cardId }) => {
|
||||
const {
|
||||
isEnrolled, title, bannerUrl, formatMessage,
|
||||
} = useCardData({
|
||||
cardId,
|
||||
});
|
||||
export const CourseCard = ({
|
||||
cardId,
|
||||
}) => {
|
||||
const isCollapsed = useIsCollapsed();
|
||||
const orientation = isCollapsed ? 'vertical' : 'horizontal';
|
||||
return (
|
||||
<div className="mb-4.5 course-card" data-testid="CourseCard">
|
||||
<Card orientation="horizontal">
|
||||
<Card orientation={orientation}>
|
||||
<div className="d-flex flex-column w-100">
|
||||
<div className="d-flex">
|
||||
<Card.ImageCap
|
||||
src={bannerUrl}
|
||||
srcAlt={formatMessage(messages.bannerAlt)}
|
||||
/>
|
||||
<Card.Body>
|
||||
<Card.Header
|
||||
title={<span data-testid="CourseCardTitle">{title}</span>}
|
||||
actions={<CourseCardMenu cardId={cardId} />}
|
||||
/>
|
||||
<Card.Section>
|
||||
<CourseCardDetails cardId={cardId} />
|
||||
</Card.Section>
|
||||
<Card.Footer
|
||||
orientation="vertical"
|
||||
textElement={<RelatedProgramsBadge cardId={cardId} />}
|
||||
>
|
||||
<CourseCardActions cardId={cardId} />
|
||||
</Card.Footer>
|
||||
</Card.Body>
|
||||
</div>
|
||||
|
||||
{isCollapsed
|
||||
? (
|
||||
<CourseCardContent cardId={cardId} orientation={orientation} />
|
||||
) : (
|
||||
<div className="d-flex">
|
||||
<CourseCardContent cardId={cardId} orientation={orientation} />
|
||||
</div>
|
||||
)}
|
||||
<div className="course-card-banners" data-testid="CourseCardBanners">
|
||||
<CourseBanner cardId={cardId} />
|
||||
<EntitlementBanner cardId={cardId} />
|
||||
{isEnrolled && <CertificateBanner cardId={cardId} />}
|
||||
<CourseCardBanners cardId={cardId} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CourseCard.propTypes = {
|
||||
cardId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
CourseCard.defaultProps = {};
|
||||
|
||||
export default CourseCard;
|
||||
|
||||
@@ -5,36 +5,21 @@ import CourseCard from '.';
|
||||
import hooks from './hooks';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
useIsCollapsed: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./components/RelatedProgramsBadge', () => 'RelatedProgramsBadge');
|
||||
jest.mock('./components/CourseCardMenu', () => 'CourseCardMenu');
|
||||
jest.mock('./components/Banners', () => ({
|
||||
CourseBanner: () => 'CourseBanner',
|
||||
CertificateBanner: () => 'CertificateBanner',
|
||||
EntitlementBanner: () => 'EntitlementBanner',
|
||||
}));
|
||||
jest.mock('./components/CourseCardActions', () => 'CourseCardActions');
|
||||
jest.mock('./components/CourseCardDetails', () => 'CourseCardDetails');
|
||||
|
||||
const dataProps = {
|
||||
title: 'hooks.title',
|
||||
bannerUrl: 'hooks.bannerUrl',
|
||||
formatMessage: jest.fn(msg => ({ formatted: msg })),
|
||||
isEnrolled: true,
|
||||
};
|
||||
jest.mock('./components/CourseCardBanners', () => 'CourseCardBanners');
|
||||
jest.mock('./components/CourseCardContent', () => 'CourseCardContent');
|
||||
|
||||
const cardId = 'test-card-id';
|
||||
|
||||
describe('CourseCard component', () => {
|
||||
test('snapshot', () => {
|
||||
hooks.mockReturnValueOnce(dataProps);
|
||||
test('snapshot: collapsed', () => {
|
||||
hooks.useIsCollapsed.mockReturnValueOnce(true);
|
||||
expect(shallow(<CourseCard cardId={cardId} />)).toMatchSnapshot();
|
||||
expect(hooks).toHaveBeenCalledWith({ cardId });
|
||||
});
|
||||
test('snapshot: not enrolled (no certificate card)', () => {
|
||||
hooks.mockReturnValueOnce({ ...dataProps, isEnrolled: true });
|
||||
test('snapshot: not collapsed', () => {
|
||||
hooks.useIsCollapsed.mockReturnValueOnce(false);
|
||||
expect(shallow(<CourseCard cardId={cardId} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,8 @@ import CourseCard from 'containers/CourseCard';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
export const useCourseListData = () => {
|
||||
const [pageNumber, setPageNumber] = React.useState(1);
|
||||
const [sortBy, setSortBy] = React.useState(SortKeys.title);
|
||||
|
||||
4
src/containers/CourseList/index.scss
Normal file
4
src/containers/CourseList/index.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
#course-list-heading-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Container, Col, Row } from '@edx/paragon';
|
||||
|
||||
import {
|
||||
thunkActions,
|
||||
@@ -25,23 +26,28 @@ export const Dashboard = () => {
|
||||
const hasAvailableDashboards = appHooks.useHasAvailableDashboards();
|
||||
const showSelectSessionModal = appHooks.useShowSelectSessionModal();
|
||||
return (
|
||||
<div className="d-flex flex-column p-2" id="course-dashboard">
|
||||
<div id="dashboard-container" className="d-flex flex-column p-2">
|
||||
{hasAvailableDashboards && <EnterpriseDashboardModal />}
|
||||
{hasCourses ? (
|
||||
<>
|
||||
<div className="d-flex" style={{ margin: 'auto' }}>
|
||||
<div className="w-100 mw-md mr-4">
|
||||
<Container fluid size="xl">
|
||||
<Row>
|
||||
<Col
|
||||
xs={{ span: 12, offset: 0 }}
|
||||
sm={{ span: 8, offset: 2 }}
|
||||
md={{ span: 12, offset: 0 }}
|
||||
lg={{ span: 10, offset: 1 }}
|
||||
xl={{ span: 8, offset: 0 }}
|
||||
className="p-0 px-4"
|
||||
>
|
||||
{showSelectSessionModal && (<SelectSessionModal />)}
|
||||
<CourseList />
|
||||
</div>
|
||||
<div id="dashboard-sidebar-container mw-xs">
|
||||
</Col>
|
||||
<Col md={12} xl={4} className="p-0 pr-4 pl-1">
|
||||
<WidgetSidebar />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmptyCourse />
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
) : (<EmptyCourse />)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#course-list-heading-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { AvatarButton, Dropdown } from '@edx/paragon';
|
||||
|
||||
import { hooks as appHooks } from 'data/redux';
|
||||
import { useIsCollapsed } from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const AuthenticatedUserDropdown = ({ username }) => {
|
||||
@@ -14,49 +15,58 @@ export const AuthenticatedUserDropdown = ({ username }) => {
|
||||
const { authenticatedUser } = React.useContext(AppContext);
|
||||
const { profileImage } = authenticatedUser;
|
||||
const dashboard = appHooks.useEnterpriseDashboardData();
|
||||
console.log({ dashboard });
|
||||
const isCollapsed = useIsCollapsed();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown className="user-dropdown">
|
||||
<Dropdown.Toggle as={AvatarButton} src={profileImage} id="user" variant="primary">
|
||||
<span data-hj-suppress className="d-none d-md-inline">
|
||||
{username}
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
<Dropdown.Header>SWITCH DASHBOARD</Dropdown.Header>
|
||||
<Dropdown.Item as="a" href="/edx-dashboard" className="active">Personal</Dropdown.Item>
|
||||
{!!dashboard && (
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
href={dashboard.url}
|
||||
key={dashboard.label}
|
||||
>
|
||||
{dashboard.label} {formatMessage(messages.dashboard)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Divider />
|
||||
<Dropdown className="user-dropdown">
|
||||
<Dropdown.Toggle
|
||||
as={AvatarButton}
|
||||
src={profileImage}
|
||||
id="user"
|
||||
variant="primary"
|
||||
>
|
||||
<span data-hj-suppress className="d-none d-md-inline">
|
||||
{username}
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="dropdown-menu-right">
|
||||
<Dropdown.Header>SWITCH DASHBOARD</Dropdown.Header>
|
||||
<Dropdown.Item as="a" href="/edx-dashboard" className="active">Personal</Dropdown.Item>
|
||||
{!!dashboard && (
|
||||
<Dropdown.Item
|
||||
as="a"
|
||||
href={dashboard.url}
|
||||
key={dashboard.label}
|
||||
>
|
||||
{dashboard.label} {formatMessage(messages.dashboard)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${username}`}>
|
||||
{formatMessage(messages.profile)}
|
||||
</Dropdown.Item>
|
||||
{isCollapsed && (
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/u/${username}`}>
|
||||
{formatMessage(messages.profile)}
|
||||
{formatMessage(messages.viewPrograms)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
|
||||
{formatMessage(messages.account)}
|
||||
)}
|
||||
<Dropdown.Item href={`${getConfig().LMS_BASE_URL}/account/settings`}>
|
||||
{formatMessage(messages.account)}
|
||||
</Dropdown.Item>
|
||||
{getConfig().ORDER_HISTORY_URL && (
|
||||
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
|
||||
{formatMessage(messages.orderHistory)}
|
||||
</Dropdown.Item>
|
||||
{getConfig().ORDER_HISTORY_URL && (
|
||||
<Dropdown.Item href={getConfig().ORDER_HISTORY_URL}>
|
||||
{formatMessage(messages.orderHistory)}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item href={getConfig().SUPPORT_URL}>
|
||||
{formatMessage(messages.help)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item href={getConfig().LOGOUT_URL}>
|
||||
{formatMessage(messages.signOut)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</>
|
||||
)}
|
||||
<Dropdown.Item href={getConfig().SUPPORT_URL}>
|
||||
{formatMessage(messages.help)}
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item href={getConfig().LOGOUT_URL}>
|
||||
{formatMessage(messages.signOut)}
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
@@ -6,7 +8,7 @@ import { Image } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const GreetingBanner = () => {
|
||||
export const GreetingBanner = ({ size }) => {
|
||||
let greetMessage;
|
||||
const hour = new Date().getHours();
|
||||
|
||||
@@ -18,20 +20,37 @@ export const GreetingBanner = () => {
|
||||
greetMessage = messages.goodMorning;
|
||||
}
|
||||
|
||||
const isSmall = size === 'small';
|
||||
|
||||
return (
|
||||
<div className="d-flex p-5 align-items-center justify-content-center">
|
||||
<div
|
||||
className={classNames(
|
||||
'd-flex align-items-center justify-content-center',
|
||||
{ 'p-5': !isSmall, 'p-3.5': isSmall },
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
style={{ width: '148px' }}
|
||||
style={{ width: isSmall ? '46px' : '148px' }}
|
||||
className="d-block"
|
||||
src={getConfig().LOGO_WHITE_URL}
|
||||
alt={getConfig().SITE_NAME}
|
||||
/>
|
||||
<div className="greetings-slash-container bg-brand-500" />
|
||||
<h1 className="text-center text-accent-b">
|
||||
<FormattedMessage {...greetMessage} />
|
||||
</h1>
|
||||
<div className={`greetings-slash-container-${size} bg-brand-500`} />
|
||||
{isSmall
|
||||
? (
|
||||
<h5 className="text-center text-accent-b">
|
||||
<FormattedMessage {...greetMessage} />
|
||||
</h5>
|
||||
) : (
|
||||
<h1 className="text-center text-accent-b">
|
||||
<FormattedMessage {...greetMessage} />
|
||||
</h1>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
GreetingBanner.propTypes = {
|
||||
size: PropTypes.oneOf('small', 'large').isRequired,
|
||||
};
|
||||
|
||||
export default GreetingBanner;
|
||||
|
||||
10
src/containers/LearnerDashboardHeader/hooks.js
Normal file
10
src/containers/LearnerDashboardHeader/hooks.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useWindowSize, breakpoints } from '@edx/paragon';
|
||||
|
||||
export const useIsCollapsed = () => {
|
||||
const { width } = useWindowSize();
|
||||
const isCollapsed = React.useMemo(() => (width <= breakpoints.large.maxWidth), [width]);
|
||||
return isCollapsed;
|
||||
};
|
||||
|
||||
export default { useIsCollapsed };
|
||||
@@ -9,28 +9,40 @@ import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
|
||||
|
||||
import GreetingBanner from './GreetingBanner';
|
||||
import ConfirmEmailBanner from './ConfirmEmailBanner';
|
||||
import { useIsCollapsed } from './hooks';
|
||||
import messages from './messages';
|
||||
import './index.scss';
|
||||
|
||||
export const LearnerDashboardHeader = () => {
|
||||
export const UserMenu = () => {
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
return authenticatedUser ? (<AuthenticatedUserDropdown username={authenticatedUser.username} />) : null;
|
||||
};
|
||||
|
||||
export const LearnerDashboardHeader = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const isCollapsed = useIsCollapsed();
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column bg-primary">
|
||||
<>
|
||||
<ConfirmEmailBanner />
|
||||
<header className="learner-dashboard-header">
|
||||
<div className="d-flex">
|
||||
<Button variant="inverse-tertiary" iconBefore={Program}>
|
||||
{formatMessage(messages.switchToProgram)}
|
||||
</Button>
|
||||
<div className="flex-grow-1" />
|
||||
{authenticatedUser && (
|
||||
<AuthenticatedUserDropdown username={authenticatedUser.username} />
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<GreetingBanner />
|
||||
</div>
|
||||
<div className="flex-column bg-primary">
|
||||
<header className="learner-dashboard-header">
|
||||
<div className="d-flex">
|
||||
{isCollapsed && (<div className="my-auto ml-1"><UserMenu /></div>)}
|
||||
{(!isCollapsed) && (
|
||||
<Button variant="inverse-tertiary" iconBefore={Program}>
|
||||
{formatMessage(messages.switchToProgram)}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex-grow-1">
|
||||
{isCollapsed && <GreetingBanner size="small" />}
|
||||
</div>
|
||||
{!isCollapsed && (<UserMenu />)}
|
||||
</div>
|
||||
</header>
|
||||
{!isCollapsed && <GreetingBanner size="large" />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
.greetings-slash-container {
|
||||
.greetings-slash-container-small {
|
||||
height: 4px;
|
||||
width: 40px;
|
||||
transform-origin: center;
|
||||
transform: rotate(-70deg);
|
||||
}
|
||||
.greetings-slash-container-large {
|
||||
height: 8px;
|
||||
width: 120px;
|
||||
transform-origin: center;
|
||||
|
||||
@@ -16,6 +16,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Profile',
|
||||
description: 'The text for the user menu Profile navigation link.',
|
||||
},
|
||||
viewPrograms: {
|
||||
id: 'leanerDashboard.menu.viewPrograms.label',
|
||||
defaultMessage: 'View Programs',
|
||||
description: 'The text for the user menu View Programs navigation link.',
|
||||
},
|
||||
account: {
|
||||
id: 'leanerDashboard.menu.account.label',
|
||||
defaultMessage: 'Account',
|
||||
|
||||
@@ -29,49 +29,58 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = `
|
||||
<p>
|
||||
Are you looking to expand your knowledge? Enrolling in a Program lets you take a series of courses in the subject that you're interested in
|
||||
</p>
|
||||
<CardGrid
|
||||
columnSizes={
|
||||
Object {
|
||||
"lg": 6,
|
||||
"xlg": 4,
|
||||
"xs": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program1",
|
||||
},
|
||||
"programUrl": "program-1-url",
|
||||
}
|
||||
}
|
||||
key="program-1-url"
|
||||
/>
|
||||
<ProgramCard
|
||||
data={
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program2",
|
||||
},
|
||||
"programUrl": "program-2-url",
|
||||
}
|
||||
}
|
||||
key="program-2-url"
|
||||
/>
|
||||
<ProgramCard
|
||||
data={
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program3",
|
||||
},
|
||||
"programUrl": "program-3-url",
|
||||
}
|
||||
}
|
||||
key="program-3-url"
|
||||
/>
|
||||
</CardGrid>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col
|
||||
key="program-1-url"
|
||||
lg={6}
|
||||
sm={12}
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program1",
|
||||
},
|
||||
"programUrl": "program-1-url",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col
|
||||
key="program-2-url"
|
||||
lg={6}
|
||||
sm={12}
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program2",
|
||||
},
|
||||
"programUrl": "program-2-url",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col
|
||||
key="program-3-url"
|
||||
lg={6}
|
||||
sm={12}
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program3",
|
||||
},
|
||||
"programUrl": "program-3-url",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</ModalDialog.Body>
|
||||
</ModalDialog>
|
||||
`;
|
||||
@@ -105,49 +114,58 @@ exports[`RelatedProgramsModal snapshot: open 1`] = `
|
||||
<p>
|
||||
Are you looking to expand your knowledge? Enrolling in a Program lets you take a series of courses in the subject that you're interested in
|
||||
</p>
|
||||
<CardGrid
|
||||
columnSizes={
|
||||
Object {
|
||||
"lg": 6,
|
||||
"xlg": 4,
|
||||
"xs": 12,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program1",
|
||||
},
|
||||
"programUrl": "program-1-url",
|
||||
}
|
||||
}
|
||||
key="program-1-url"
|
||||
/>
|
||||
<ProgramCard
|
||||
data={
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program2",
|
||||
},
|
||||
"programUrl": "program-2-url",
|
||||
}
|
||||
}
|
||||
key="program-2-url"
|
||||
/>
|
||||
<ProgramCard
|
||||
data={
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program3",
|
||||
},
|
||||
"programUrl": "program-3-url",
|
||||
}
|
||||
}
|
||||
key="program-3-url"
|
||||
/>
|
||||
</CardGrid>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col
|
||||
key="program-1-url"
|
||||
lg={6}
|
||||
sm={12}
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program1",
|
||||
},
|
||||
"programUrl": "program-1-url",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col
|
||||
key="program-2-url"
|
||||
lg={6}
|
||||
sm={12}
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program2",
|
||||
},
|
||||
"programUrl": "program-2-url",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col
|
||||
key="program-3-url"
|
||||
lg={6}
|
||||
sm={12}
|
||||
>
|
||||
<ProgramCard
|
||||
data={
|
||||
Object {
|
||||
"programData": Object {
|
||||
"dataFor": "program3",
|
||||
},
|
||||
"programUrl": "program-3-url",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</ModalDialog.Body>
|
||||
</ModalDialog>
|
||||
`;
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ProgramCard = ({ data }) => {
|
||||
);
|
||||
return (
|
||||
<Card
|
||||
className="program-card d-inline-block bg-primary-500 text-white pb-3.5"
|
||||
className="program-card mx-auto bg-primary-500 text-white mb-3.5 pb-3.5"
|
||||
style={{ width: '18rem', color: 'white' }}
|
||||
>
|
||||
<Card.ImageCap
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`RelatedProgramsModal ProgramCard snapshot 1`] = `
|
||||
<Card
|
||||
className="program-card d-inline-block bg-primary-500 text-white pb-3.5"
|
||||
className="program-card mx-auto bg-primary-500 text-white mb-3.5 pb-3.5"
|
||||
style={
|
||||
Object {
|
||||
"color": "white",
|
||||
|
||||
@@ -3,7 +3,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { CardGrid, ModalDialog } from '@edx/paragon';
|
||||
import {
|
||||
Container, Row, Col, ModalDialog,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import ProgramCard from './components/ProgramCard';
|
||||
import messages from './messages';
|
||||
@@ -36,13 +38,15 @@ export const RelatedProgramsModal = ({
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body className="pl-0 overflow-hidden">
|
||||
<p>{formatMessage(messages.description)}</p>
|
||||
<CardGrid
|
||||
columnSizes={{ lg: 6, xlg: 4, xs: 12 }}
|
||||
>
|
||||
{relatedPrograms.map((programData) => (
|
||||
<ProgramCard key={programData.programUrl} data={programData} />
|
||||
))}
|
||||
</CardGrid>
|
||||
<Container>
|
||||
<Row>
|
||||
{relatedPrograms.map((programData) => (
|
||||
<Col key={programData.programUrl} lg={6} sm={12}>
|
||||
<ProgramCard data={programData} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Container>
|
||||
</ModalDialog.Body>
|
||||
</ModalDialog>
|
||||
);
|
||||
|
||||
@@ -11,26 +11,15 @@ import './index.scss';
|
||||
export const WidgetSidebar = () => (
|
||||
<div className="widget-sidebar">
|
||||
<div className="d-flex">
|
||||
|
||||
{/* <img src='more-courses-sidewidget.svg' />
|
||||
<div>
|
||||
<h3>
|
||||
<FormattedMessage {...messages.lookingForChallengePrompt} />
|
||||
</h3>
|
||||
<Hyperlink variant='brand' destination='#'>
|
||||
<FormattedMessage {...messages.findCoursesButton} />
|
||||
</Hyperlink>
|
||||
</div> */}
|
||||
|
||||
<Card orientation="horizontal" className="mb-4">
|
||||
<Card orientation="horizontal">
|
||||
<Card.ImageCap
|
||||
src={moreCoursesSVG}
|
||||
srcAlt="course side widget"
|
||||
/>
|
||||
<Card.Body className="m-auto pr-2">
|
||||
<h3>
|
||||
<h4>
|
||||
<FormattedMessage {...messages.lookingForChallengePrompt} />
|
||||
</h3>
|
||||
</h4>
|
||||
<Hyperlink variant="brand" destination="#">
|
||||
<FormattedMessage {...messages.findCoursesButton} />
|
||||
</Hyperlink>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
@import "@edx/paragon/scss/core/core";
|
||||
.widget-sidebar {
|
||||
margin-top: map-get($spacers, 5);
|
||||
width: 400px;
|
||||
flex-shrink: 0;
|
||||
padding-left: map-get($spacers, 2);
|
||||
padding-right: map-get($spacers, 2);
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
.widget-sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user