Skip to content

Commit

Permalink
✨(frontend) add mocked organization to
Browse files Browse the repository at this point in the history
...TeacherDashboardProfileSidebar

Part of teacher dashboard development, the sidebar contains
links to user's organizations.
  • Loading branch information
rlecellier committed Apr 18, 2023
1 parent 56fa25c commit 6ab4b4a
Show file tree
Hide file tree
Showing 24 changed files with 411 additions and 60 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -740,7 +740,7 @@ jobs:
command: |
yarn init -y
cp ~/fun/src/frontend/yarn.lock .
yarn add webpack webpack-cli babel-loader source-map-loader ~/fun/src/frontend
yarn add webpack webpack-cli babel-loader source-map-loader file-loader ~/fun/src/frontend
echo '{"extends": "./node_modules/richie-education/tsconfig.json", "include": ["./**/*"]}' > tsconfig.json
- run:
name: Create an overridden React component
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Versioning](https://semver.org/spec/v2.0.0.html).
### Added

- Add head_js block into base html template
- list teacher's organizations in the teacher dashboard sidebar.

### Fixed

Expand Down
18 changes: 18 additions & 0 deletions src/frontend/js/api/joanie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ const getRoutes = () => {
create: `${baseUrl}/wishlist/`,
delete: `${baseUrl}/wishlist/:id/`,
},
organizations: {
get: `${baseUrl}/organizations/:id/`,
},
},
products: {
get: `${baseUrl}/products/:id/`,
Expand Down Expand Up @@ -298,6 +301,21 @@ const API = (): Joanie.API => {
}).then(checkStatus);
},
},
organizations: {
get: async (filters) => {
let url;
const { id = '', ...queryParameters } = filters || {};

if (id) url = ROUTES.user.organizations.get.replace(':id', id);
else url = ROUTES.user.organizations.get.replace(':id/', '');

if (!ObjectHelper.isEmpty(queryParameters)) {
url += '?' + queryString.stringify(queryParameters);
}

return fetchWithJWT(url, { method: 'GET' }).then(checkStatus);
},
},
},
products: {
get: async (filters = {}) => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions src/frontend/js/api/mocks/joanie/organizations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { rest } from 'msw';
import { getAPIEndpoint } from 'api/joanie';

export interface OrganizationMock {
id: string;
code: string;
title: string;
logo: {
filename: string;
url: string;
height: number;
width: number;
};
}

export const listOrganizations = rest.get<OrganizationMock[]>(
`${getAPIEndpoint()}/organizations`,
(_req, res, ctx) => {
const cover001 = require('./assets/organization_cover_001.jpg');
const cover002 = require('./assets/organization_cover_002.jpg');
const organisations: OrganizationMock[] = [
{
id: 'AAA',
code: 'code__AAA',
title: 'Univesité de Rennes',
logo: {
filename: 'organization_cover_001.jpg',
url: cover001.default,
height: 113,
width: 113,
},
},
{
id: 'BBB',
code: 'code__BBB',
title: 'Univesité de Paris',
logo: {
filename: 'organization_cover_002.jpg',
url: cover002.default,
height: 113,
width: 113,
},
},
];
return res(ctx.status(200), ctx.json(organisations));
},
);
31 changes: 31 additions & 0 deletions src/frontend/js/hooks/useOrganizations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defineMessages } from 'react-intl';

import { OrganizationMock } from 'api/mocks/joanie/organizations';
import { useJoanieApi } from 'contexts/JoanieApiContext';
import { useResource, useResources, UseResourcesProps } from '../useResources';

const messages = defineMessages({
errorGet: {
id: 'hooks.useOrganizations.errorSelect',
description: 'Error message shown to the user when organizations fetch request fails.',
defaultMessage: 'An error occurred while fetching organizations. Please retry later.',
},
errorNotFound: {
id: 'hooks.useOrganizations.errorNotFound',
description: 'Error message shown to the user when no organizations matches.',
defaultMessage: 'Cannot find the organization',
},
});

/**
* Joanie Api hook to retrieve organizations
* owned by the authenticated user.
*/
const props: UseResourcesProps<OrganizationMock> = {
queryKey: ['organizations'],
apiInterface: () => useJoanieApi().user.organizations,
session: true,
messages,
};
export const useOrganizations = useResources<OrganizationMock>(props);
export const useOrganization = useResource<OrganizationMock>(props);
16 changes: 16 additions & 0 deletions src/frontend/js/pages/TeacherCoursesDashboardLoader/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useIntl } from 'react-intl';
import { DashboardLayout } from 'widgets/Dashboard/components/DashboardLayout';
import { TeacherProfileDashboardSidebar } from 'widgets/Dashboard/components/TeacherProfileDashboardSidebar';
import RouteInfo from 'widgets/Dashboard/components/RouteInfo';
import { getDashboardRouteLabel } from 'widgets/Dashboard/utils/dashboardRoutes';
import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherRouteMessages';

export const TeacherCoursesDashboardLoader = () => {
const intl = useIntl();
const getRouteLabel = getDashboardRouteLabel(intl);
return (
<DashboardLayout sidebar={<TeacherProfileDashboardSidebar />}>
<RouteInfo title={getRouteLabel(TeacherDashboardPaths.TEACHER_NOTIFICATIONS)} />,
</DashboardLayout>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { TeacherOrganizationDashboardSidebar } from 'widgets/Dashboard/component
const messages = defineMessages({
loading: {
defaultMessage: 'Loading organization ...',
description: 'Message displayed while loading an organization',
description: "Message displayed while loading courses on the teacher's dashboard'",
id: 'components.TeacherOrganizationCourseDashboardLoader.loading',
},
});
Expand Down
8 changes: 8 additions & 0 deletions src/frontend/js/types/Joanie.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Priority, StateCTA, StateText } from 'types';
import type { Nullable } from 'types/utils';
import { Resource, ResourcesQuery } from 'hooks/useResources';
import { OrganizationMock } from '../api/mocks/joanie/organizations';

// - Generic
export interface PaginatedResponse<T> {
Expand Down Expand Up @@ -292,6 +293,13 @@ interface APIUser {
create(payload: UserWishlistCreationPayload): Promise<UserWishlistCourse>;
delete(id: UserWishlistCourse['id']): Promise<void>;
};
organizations: {
get<Filters extends ResourcesQuery = ResourcesQuery>(
filters?: Filters,
): Filters extends { id: string }
? Promise<Nullable<OrganizationMock>>
: Promise<OrganizationMock[]>;
};
}

export interface API {
Expand Down
14 changes: 13 additions & 1 deletion src/frontend/js/utils/test/factories/joanie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ export const TargetCourseFactory = createSpec({
});

export const OrganizationFactory = createSpec({
id: faker.random.alphaNumeric(5),
code: faker.random.alphaNumeric(5),
title: faker.random.words(1),
logo: {
filename: faker.random.words(1),
url: derived(({ id }: { id: string }) => `/organizations/${id}`),
height: 40,
width: 60,
},
});

export const OrganizationLightFactory = createSpec({
code: faker.random.alphaNumeric(5),
title: faker.random.words(1),
});
Expand Down Expand Up @@ -82,7 +94,7 @@ export const CourseRunFactory = (scopes?: { course: Boolean }) => {

export const CourseFactory = createSpec({
code: faker.random.alphaNumeric(5),
organization: OrganizationFactory,
organization: OrganizationLightFactory,
title: faker.unique(faker.random.words(Math.ceil(Math.random() * 3))),
products: derived(() => ProductFactory.generate(1, 3)),
course_runs: [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineMessages, FormattedMessage } from 'react-intl';
import { matchPath, NavLink, useLocation, useNavigate } from 'react-router-dom';
import { ChangeEvent, useMemo, useRef } from 'react';
import { ChangeEvent, PropsWithChildren, useMemo, useRef } from 'react';
import { useSession } from 'contexts/SessionContext';
import { DashboardAvatar } from 'widgets/Dashboard/components/DashboardAvatar';

Expand All @@ -17,13 +17,18 @@ const messages = defineMessages({
},
});

interface DashboardSidebarProps {
interface DashboardSidebarProps extends PropsWithChildren {
menuLinks: Record<string, string>[];
header: string;
subHeader: string;
}

export const DashboardSidebar = ({ menuLinks, header, subHeader }: DashboardSidebarProps) => {
export const DashboardSidebar = ({
children,
menuLinks,
header,
subHeader,
}: DashboardSidebarProps) => {
const { user } = useSession();
const location = useLocation();
const navigate = useNavigate();
Expand Down Expand Up @@ -77,6 +82,7 @@ export const DashboardSidebar = ({ menuLinks, header, subHeader }: DashboardSide
))}
</ul>
</div>
{children}
</aside>
);
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.dashboard-sidebar {
&__organization-section {
// padding: rem-calc(5px) rem-calc(8px) rem-calc(40px);
padding: rem-calc(20px) rem-calc(8px) rem-calc(40px);

&__title {
font-size: rem-calc(16px);
font-weight: normal;
color: r-theme-val(dashboard-sidebar-organization-link-title, base-color);
}

&__link_list {
display: flex;
flex-wrap: wrap;
flex-direction: row;
gap: rem-calc(5px);
margin-top: rem-calc(10px);
}

&__link {
flex: 0 0 32%;
display: flex;
border: thin solid r-theme-val(organization-thumb, border-color);
border-radius: rem-calc(8px);
overflow: hidden;
justify-content: center;
align-items: center;
padding: rem-calc(2px);
&:hover,
&:focus,
&:active {
border-color: r-theme-val(organization-thumb, border-color-hover);
box-shadow: 0 0 rem-calc(3px) r-theme-val(organization-thumb, border-color);
}
&__img {
object-fit: cover;
object-position: center center;
aspect-ratio: 2/1;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { OrganizationMock } from 'api/mocks/joanie/organizations';
import { getDashboardRoutePath } from 'widgets/Dashboard/utils/dashboardRoutes';
import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherRouteMessages';

export const messages = defineMessages({
organizationsTitle: {
id: 'components.TeacherProfileDashboardSidebar.OrganizationLinks.organizationsTitle',
description: 'Title of the organizations section',
defaultMessage: 'My universities',
},
organizationLinkTitle: {
id: 'components.TeacherProfileDashboardSidebar.OrganizationLinks.organizationLinkTitle',
description: 'Organization link title',
defaultMessage: 'Link to organization "{organizationTitle}"',
},
});

interface OrganizationLinksProps {
organizations: OrganizationMock[];
}

const OrganizationLinks = ({ organizations }: OrganizationLinksProps) => {
const intl = useIntl();
return (
<div className="dashboard-sidebar__organization-section" data-testid="organization-links">
<span className="dashboard-sidebar__organization-section__title">
<FormattedMessage {...messages.organizationsTitle} />
</span>
<div className="dashboard-sidebar__organization-section__link_list">
{organizations.map((organization) => (
<Link
key={organization.id}
to={getDashboardRoutePath(intl)(TeacherDashboardPaths.ORGANIZATION_COURSES, {
organizationId: organization.id,
})}
className="dashboard-sidebar__organization-section__link"
title={intl.formatMessage(messages.organizationLinkTitle, {
organizationTitle: organization.title,
})}
>
<img
className="dashboard-sidebar__organization-section__link__img"
alt={organization.title}
src={organization.logo.url}
/>
</Link>
))}
</div>
</div>
);
};

export default OrganizationLinks;
Loading

0 comments on commit 6ab4b4a

Please sign in to comment.