Skip to content

Commit

Permalink
Create Analytics page and Leadership tab (#4181)
Browse files Browse the repository at this point in the history
* Everything minus tests

* linting

* add flag

* add some basic tests

* add more tests

* add menu icon and pagination

* fix tests

* fix test

* adjust styles

* remove comments
  • Loading branch information
lctrt authored Mar 5, 2024
1 parent 3fd54f5 commit 3e9f617
Show file tree
Hide file tree
Showing 27 changed files with 672 additions and 0 deletions.
14 changes: 14 additions & 0 deletions apps/crn-frontend/src/AuthenticatedApp.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { isEnabled } from '@asap-hub/flags';
import { SkeletonHeaderFrame as Frame } from '@asap-hub/frontend-utils';
import { Layout, Loading, NotFoundPage } from '@asap-hub/react-components';
import { useAuth0CRN, useCurrentUserCRN } from '@asap-hub/react-context';
import {
about,
analytics,
dashboard,
discover,
events,
Expand Down Expand Up @@ -36,13 +38,17 @@ const loadTags = () => import(/* webpackChunkName: "tags" */ './tags/Routes');
const loadAbout = () =>
import(/* webpackChunkName: "about" */ './about/Routes');

const loadAnalytics = () =>
import(/* webpackChunkName: "analytics" */ './analytics/Routes');

const News = lazy(loadNews);
const Network = lazy(loadNetwork);
const SharedResearch = lazy(loadSharedResearch);
const Dashboard = lazy(loadDashboard);
const Discover = lazy(loadDiscover);
const Events = lazy(loadEvents);
const About = lazy(loadAbout);
const Analytics = lazy(loadAnalytics);
const Tags = lazy(loadTags);

const AuthenticatedApp: FC<Record<string, never>> = () => {
Expand All @@ -63,6 +69,7 @@ const AuthenticatedApp: FC<Record<string, never>> = () => {
.then(loadSharedResearch)
.then(loadDiscover)
.then(loadAbout)
.then(loadAnalytics)
.then(loadEvents)
.then(loadTags);
}, []);
Expand Down Expand Up @@ -134,6 +141,13 @@ const AuthenticatedApp: FC<Record<string, never>> = () => {
<About />
</Frame>
</Route>
{isEnabled('ANALYTICS') && (
<Route path={analytics.template}>
<Frame title="Analytics">
<Analytics />
</Frame>
</Route>
)}
<Route path={news.template}>
<Frame title="News">
<News />
Expand Down
67 changes: 67 additions & 0 deletions apps/crn-frontend/src/analytics/Analytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { FC, useState } from 'react';
import { AnalyticsPageBody } from '@asap-hub/react-components';
import { useMemberships } from './state';
import { usePagination, usePaginationParams } from '../hooks';

type MetricResponse = {
id: string;
displayName: string;
workingGroupLeadershipRoleCount: number;
workingGroupPreviousLeadershipRoleCount: number;
workingGroupMemberCount: number;
workingGroupPreviousMemberCount: number;

interestGroupLeadershipRoleCount: number;
interestGroupPreviousLeadershipRoleCount: number;
interestGroupMemberCount: number;
interestGroupPreviousMemberCount: number;
};

const getDataForMetric = (
data: MetricResponse[],
metric: 'workingGroup' | 'interestGroup',
) => {
if (metric === 'workingGroup') {
return data.map((row) => ({
id: row.id,
name: row.displayName,
leadershipRoleCount: row.workingGroupLeadershipRoleCount,
previousLeadershipRoleCount: row.workingGroupPreviousLeadershipRoleCount,
memberCount: row.workingGroupMemberCount,
previousMemberCount: row.workingGroupPreviousMemberCount,
}));
}
return data.map((row) => ({
id: row.id,
name: row.displayName,
leadershipRoleCount: row.interestGroupLeadershipRoleCount,
previousLeadershipRoleCount: row.interestGroupPreviousLeadershipRoleCount,
memberCount: row.interestGroupMemberCount,
previousMemberCount: row.interestGroupPreviousMemberCount,
}));
};

const About: FC<Record<string, never>> = () => {
const [metric, setMetric] = useState<'workingGroup' | 'interestGroup'>(
'workingGroup',
);
const { data } = useMemberships();
const { currentPage, pageSize } = usePaginationParams();
const { numberOfPages, renderPageHref } = usePagination(
data.length,
pageSize,
);

return (
<AnalyticsPageBody
metric={metric}
setMetric={setMetric}
data={getDataForMetric(data, metric)}
currentPageIndex={currentPage}
numberOfPages={numberOfPages}
renderPageHref={renderPageHref}
/>
);
};

export default About;
32 changes: 32 additions & 0 deletions apps/crn-frontend/src/analytics/Routes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { SkeletonBodyFrame as Frame } from '@asap-hub/frontend-utils';
import { AnalyticsPage } from '@asap-hub/react-components';
import { FC, lazy, useEffect } from 'react';
import { Route, Switch, useRouteMatch } from 'react-router-dom';

const loadAnalytics = () =>
import(/* webpackChunkName: "analytics" */ './Analytics');

const AnalyticsBody = lazy(loadAnalytics);

const About: FC<Record<string, never>> = () => {
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
loadAnalytics();
}, []);

const { path } = useRouteMatch();

return (
<Switch>
<Route exact path={path}>
<AnalyticsPage>
<Frame title="Analytics">
<AnalyticsBody />
</Frame>
</AnalyticsPage>
</Route>
</Switch>
);
};

export default About;
73 changes: 73 additions & 0 deletions apps/crn-frontend/src/analytics/__tests__/Analytics.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';

import Analytics from '../Analytics';
import { getMemberships } from '../api';

jest.mock('../api');

afterEach(() => {
jest.clearAllMocks();
});

const mockGetMemberships = getMemberships as jest.MockedFunction<
typeof getMemberships
>;

const data = [
{
id: '1',
displayName: 'Team 1',
workingGroupLeadershipRoleCount: 1,
workingGroupPreviousLeadershipRoleCount: 2,
workingGroupMemberCount: 3,
workingGroupPreviousMemberCount: 4,
interestGroupLeadershipRoleCount: 5,
interestGroupPreviousLeadershipRoleCount: 6,
interestGroupMemberCount: 7,
interestGroupPreviousMemberCount: 8,
},
{
id: '2',
displayName: 'Team 2',
workingGroupLeadershipRoleCount: 2,
workingGroupPreviousLeadershipRoleCount: 3,
workingGroupMemberCount: 4,
workingGroupPreviousMemberCount: 5,
interestGroupLeadershipRoleCount: 4,
interestGroupPreviousLeadershipRoleCount: 3,
interestGroupMemberCount: 2,
interestGroupPreviousMemberCount: 1,
},
];

const renderPage = async () => {
render(
<MemoryRouter initialEntries={['/analytics']}>
<Analytics />
</MemoryRouter>,
);
};

it('renders with working group data', async () => {
mockGetMemberships.mockReturnValue(data);

await renderPage();
expect(
screen.getAllByText('Working Group Leadership & Membership').length,
).toBe(2);
});

it('renders with interest group data', async () => {
mockGetMemberships.mockReturnValue(data);
const label = 'Interest Group Leadership & Membership';

await renderPage();
const input = screen.getByRole('textbox', { hidden: false });

userEvent.click(input);
userEvent.click(screen.getByText(label));

expect(screen.getAllByText(label).length).toBe(2);
});
40 changes: 40 additions & 0 deletions apps/crn-frontend/src/analytics/__tests__/Routes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { render, screen } from '@testing-library/react';
import { mockConsoleError } from '@asap-hub/dom-test-utils';
import { MemoryRouter, Route } from 'react-router-dom';
import { analytics } from '@asap-hub/routing';

import About from '../Routes';
import { getMemberships } from '../api';

jest.mock('../api');
mockConsoleError();
afterEach(() => {
jest.clearAllMocks();
});

const mockGetMemberships = getMemberships as jest.MockedFunction<
typeof getMemberships
>;

const renderPage = async () => {
render(
<MemoryRouter initialEntries={['/analytics']}>
<Route path={analytics.template}>
<About />
</Route>
</MemoryRouter>,
);
};

describe('Analytics page', () => {
it('renders the Analytics Page successfully', async () => {
mockGetMemberships.mockReturnValue([]);

await renderPage();
expect(
await screen.findByText(/Analytics/i, {
selector: 'h1',
}),
).toBeVisible();
});
});
7 changes: 7 additions & 0 deletions apps/crn-frontend/src/analytics/__tests__/api.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getMemberships } from '../api';

describe('getMemberships', () => {
it('returns data', () => {
expect(getMemberships().length).toBe(2);
});
});
29 changes: 29 additions & 0 deletions apps/crn-frontend/src/analytics/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export const getMemberships = () => {
const fakeData = [
{
id: '1',
displayName: 'Team 1',
workingGroupLeadershipRoleCount: 1,
workingGroupPreviousLeadershipRoleCount: 2,
workingGroupMemberCount: 3,
workingGroupPreviousMemberCount: 4,
interestGroupLeadershipRoleCount: 5,
interestGroupPreviousLeadershipRoleCount: 6,
interestGroupMemberCount: 7,
interestGroupPreviousMemberCount: 8,
},
{
id: '2',
displayName: 'Team 2',
workingGroupLeadershipRoleCount: 2,
workingGroupPreviousLeadershipRoleCount: 3,
workingGroupMemberCount: 4,
workingGroupPreviousMemberCount: 5,
interestGroupLeadershipRoleCount: 4,
interestGroupPreviousLeadershipRoleCount: 3,
interestGroupMemberCount: 2,
interestGroupPreviousMemberCount: 1,
},
];
return fakeData;
};
5 changes: 5 additions & 0 deletions apps/crn-frontend/src/analytics/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { getMemberships } from './api';

export const useMemberships = () => ({
data: getMemberships(),
});
1 change: 1 addition & 0 deletions packages/flags/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type Flag =
| 'PERSISTENT_EXAMPLE'
| 'VERSION_RESEARCH_OUTPUT'
| 'ANALYTICS'
| 'DISPLAY_EVENTS';

export type Flags = Partial<Record<Flag, boolean | undefined>>;
Expand Down
23 changes: 23 additions & 0 deletions packages/react-components/src/icons/analytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* istanbul ignore file */

const analytics = (
<svg
width="24"
height="24"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Analytics</title>
<path
d="M6.78571 4H4V40.2143C4 40.9531 4.29349 41.6617 4.81592 42.1841C5.33834 42.7065 6.0469 43 6.78571 43H43V40.2143H6.78571V4Z"
fill="#4D646B"
/>
<path
d="M42.9999 13.75H33.2499V16.5357H38.2502L27.6784 27.1075L21.7031 21.1182C21.5736 20.9877 21.4195 20.884 21.2498 20.8133C21.0801 20.7426 20.898 20.7062 20.7141 20.7062C20.5303 20.7062 20.3482 20.7426 20.1785 20.8133C20.0088 20.884 19.8547 20.9877 19.7252 21.1182L9.57129 31.2861L11.5352 33.25L20.7141 24.0711L26.6895 30.0604C26.819 30.1909 26.973 30.2945 27.1428 30.3652C27.3125 30.436 27.4946 30.4724 27.6784 30.4724C27.8623 30.4724 28.0444 30.436 28.2141 30.3652C28.3838 30.2945 28.5379 30.1909 28.6674 30.0604L40.2141 18.4996V23.5H42.9999V13.75Z"
fill="#4D646B"
/>
</svg>
);

export default analytics;
2 changes: 2 additions & 0 deletions packages/react-components/src/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as aboutIcon } from './about';
export { default as analyticsIcon } from './analytics';
export { default as actionIcon } from './action';
export { default as alumniBadgeIcon } from './alumni-badge';
export { default as article } from './article';
Expand Down Expand Up @@ -59,6 +60,7 @@ export { default as infoInfoIcon } from './info-info';
export { default as LabIcon } from './lab';
export { default as labResource } from './lab-resource';
export { default as lastPageIcon } from './last-page';
export { default as LeadershipIcon } from './leadership';
export { default as learnIcon } from './learn';
export { default as LibraryIcon } from './shared-research';
export { default as linkIcon } from './link';
Expand Down
42 changes: 42 additions & 0 deletions packages/react-components/src/icons/leadership.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* istanbul ignore file */

import { FC } from 'react';

interface LeadershipIconProps {
readonly color?: string;
readonly size?: number;
}
const Leadership: FC<LeadershipIconProps> = ({
color = '#4D646B',
size = 24,
}) => (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<title>Leadership</title>
<path
d="M7.50903 14.8129L3.24464 7.44625C3.06076 7.12841 2.97428 6.76357 2.99594 6.39701C3.0176 6.03045 3.14645 5.67833 3.36648 5.38435L4.87542 3.37869C5.05002 3.14589 5.27642 2.95694 5.5367 2.8268C5.79698 2.69666 6.08398 2.62891 6.37498 2.62891H17.6217C17.9127 2.62891 18.1997 2.69666 18.46 2.8268C18.7203 2.95694 18.9467 3.14589 19.1213 3.37869L20.6208 5.38435C20.8423 5.67738 20.9728 6.02902 20.9962 6.39559C21.0195 6.76217 20.9346 7.12752 20.752 7.44625L16.4877 14.8129M11.0611 12.0012L5.55022 2.81635M12.9356 12.0012L18.4465 2.81635M8.24944 7.31504H15.7473"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12.0006 21.3723C14.5887 21.3723 16.6867 19.2742 16.6867 16.6861C16.6867 14.0981 14.5887 12 12.0006 12C9.41251 12 7.31445 14.0981 7.31445 16.6861C7.31445 19.2742 9.41251 21.3723 12.0006 21.3723Z"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M11.9969 17.6245V15.75H11.5283"
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

export default Leadership;
Loading

0 comments on commit 3e9f617

Please sign in to comment.