diff --git a/apps/crn-frontend/src/AuthenticatedApp.tsx b/apps/crn-frontend/src/AuthenticatedApp.tsx index bcadcb95f2..2f988a734e 100644 --- a/apps/crn-frontend/src/AuthenticatedApp.tsx +++ b/apps/crn-frontend/src/AuthenticatedApp.tsx @@ -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, @@ -36,6 +38,9 @@ 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); @@ -43,6 +48,7 @@ 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> = () => { @@ -63,6 +69,7 @@ const AuthenticatedApp: FC> = () => { .then(loadSharedResearch) .then(loadDiscover) .then(loadAbout) + .then(loadAnalytics) .then(loadEvents) .then(loadTags); }, []); @@ -134,6 +141,13 @@ const AuthenticatedApp: FC> = () => { + {isEnabled('ANALYTICS') && ( + + + + + + )} diff --git a/apps/crn-frontend/src/analytics/Analytics.tsx b/apps/crn-frontend/src/analytics/Analytics.tsx new file mode 100644 index 0000000000..192dfd1ad4 --- /dev/null +++ b/apps/crn-frontend/src/analytics/Analytics.tsx @@ -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> = () => { + const [metric, setMetric] = useState<'workingGroup' | 'interestGroup'>( + 'workingGroup', + ); + const { data } = useMemberships(); + const { currentPage, pageSize } = usePaginationParams(); + const { numberOfPages, renderPageHref } = usePagination( + data.length, + pageSize, + ); + + return ( + + ); +}; + +export default About; diff --git a/apps/crn-frontend/src/analytics/Routes.tsx b/apps/crn-frontend/src/analytics/Routes.tsx new file mode 100644 index 0000000000..9b47fe428d --- /dev/null +++ b/apps/crn-frontend/src/analytics/Routes.tsx @@ -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> = () => { + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + loadAnalytics(); + }, []); + + const { path } = useRouteMatch(); + + return ( + + + + + + + + + + ); +}; + +export default About; diff --git a/apps/crn-frontend/src/analytics/__tests__/Analytics.test.tsx b/apps/crn-frontend/src/analytics/__tests__/Analytics.test.tsx new file mode 100644 index 0000000000..842bb03f11 --- /dev/null +++ b/apps/crn-frontend/src/analytics/__tests__/Analytics.test.tsx @@ -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( + + + , + ); +}; + +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); +}); diff --git a/apps/crn-frontend/src/analytics/__tests__/Routes.test.tsx b/apps/crn-frontend/src/analytics/__tests__/Routes.test.tsx new file mode 100644 index 0000000000..ac19e3851b --- /dev/null +++ b/apps/crn-frontend/src/analytics/__tests__/Routes.test.tsx @@ -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( + + + + + , + ); +}; + +describe('Analytics page', () => { + it('renders the Analytics Page successfully', async () => { + mockGetMemberships.mockReturnValue([]); + + await renderPage(); + expect( + await screen.findByText(/Analytics/i, { + selector: 'h1', + }), + ).toBeVisible(); + }); +}); diff --git a/apps/crn-frontend/src/analytics/__tests__/api.test.tsx b/apps/crn-frontend/src/analytics/__tests__/api.test.tsx new file mode 100644 index 0000000000..46f8a77d07 --- /dev/null +++ b/apps/crn-frontend/src/analytics/__tests__/api.test.tsx @@ -0,0 +1,7 @@ +import { getMemberships } from '../api'; + +describe('getMemberships', () => { + it('returns data', () => { + expect(getMemberships().length).toBe(2); + }); +}); diff --git a/apps/crn-frontend/src/analytics/api.ts b/apps/crn-frontend/src/analytics/api.ts new file mode 100644 index 0000000000..b7553a0e9c --- /dev/null +++ b/apps/crn-frontend/src/analytics/api.ts @@ -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; +}; diff --git a/apps/crn-frontend/src/analytics/state.ts b/apps/crn-frontend/src/analytics/state.ts new file mode 100644 index 0000000000..194e1f5620 --- /dev/null +++ b/apps/crn-frontend/src/analytics/state.ts @@ -0,0 +1,5 @@ +import { getMemberships } from './api'; + +export const useMemberships = () => ({ + data: getMemberships(), +}); diff --git a/packages/flags/src/index.ts b/packages/flags/src/index.ts index 47982a9e09..4738f04b59 100644 --- a/packages/flags/src/index.ts +++ b/packages/flags/src/index.ts @@ -1,6 +1,7 @@ export type Flag = | 'PERSISTENT_EXAMPLE' | 'VERSION_RESEARCH_OUTPUT' + | 'ANALYTICS' | 'DISPLAY_EVENTS'; export type Flags = Partial>; diff --git a/packages/react-components/src/icons/analytics.tsx b/packages/react-components/src/icons/analytics.tsx new file mode 100644 index 0000000000..e96ed42984 --- /dev/null +++ b/packages/react-components/src/icons/analytics.tsx @@ -0,0 +1,23 @@ +/* istanbul ignore file */ + +const analytics = ( + + Analytics + + + +); + +export default analytics; diff --git a/packages/react-components/src/icons/index.ts b/packages/react-components/src/icons/index.ts index 2362643d47..8bbf0a6252 100644 --- a/packages/react-components/src/icons/index.ts +++ b/packages/react-components/src/icons/index.ts @@ -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'; @@ -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'; diff --git a/packages/react-components/src/icons/leadership.tsx b/packages/react-components/src/icons/leadership.tsx new file mode 100644 index 0000000000..b0ab51d8b1 --- /dev/null +++ b/packages/react-components/src/icons/leadership.tsx @@ -0,0 +1,42 @@ +/* istanbul ignore file */ + +import { FC } from 'react'; + +interface LeadershipIconProps { + readonly color?: string; + readonly size?: number; +} +const Leadership: FC = ({ + color = '#4D646B', + size = 24, +}) => ( + + Leadership + + + + +); + +export default Leadership; diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index 91f9f44b10..29ad509635 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -131,6 +131,7 @@ export { InterestGroupTeamsTabbedCard, HelpSection, JoinEvent, + LeadershipMembershipTable, MainNavigation, MenuHeader, NewsCard, @@ -191,6 +192,9 @@ export { AboutPage, AboutPageBody, AboutPageHeader, + AnalyticsPage, + AnalyticsPageBody, + AnalyticsPageHeader, BasicLayout, BiographyModal, ContactInfoModal, diff --git a/packages/react-components/src/organisms/LeadershipMembershipTable.tsx b/packages/react-components/src/organisms/LeadershipMembershipTable.tsx new file mode 100644 index 0000000000..9d9014b717 --- /dev/null +++ b/packages/react-components/src/organisms/LeadershipMembershipTable.tsx @@ -0,0 +1,97 @@ +import { css } from '@emotion/react'; +import { Card } from '../atoms'; +import { charcoal, steel } from '../colors'; +import { perRem, tabletScreen } from '../pixels'; + +const container = css({ + display: 'grid', + padding: `${32 / perRem}em ${24 / perRem}em ${15 / perRem}em`, +}); + +const gridTitleStyles = css({ + display: 'none', + [`@media (min-width: ${tabletScreen.min}px)`]: { + display: 'inherit', + paddingBottom: `${16 / perRem}em`, + }, +}); + +const rowTitleStyles = css({ + paddingTop: `${32 / perRem}em`, + paddingBottom: `${16 / perRem}em`, + ':first-of-type': { paddingTop: 0 }, + [`@media (min-width: ${tabletScreen.min}px)`]: { display: 'none' }, +}); + +const rowStyles = css({ + display: 'grid', + paddingTop: `${20 / perRem}em`, + paddingBottom: 0, + borderBottom: `1px solid ${steel.rgb}`, + ':first-of-type': { + borderBottom: 'none', + }, + ':last-child': { + borderBottom: 'none', + marginBottom: 0, + paddingBottom: 0, + }, + [`@media (min-width: ${tabletScreen.min}px)`]: { + gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr', + columnGap: `${15 / perRem}em`, + paddingTop: `${0 / perRem}em`, + paddingBottom: 0, + borderBottom: `1px solid ${steel.rgb}`, + }, +}); + +const titleStyles = css({ fontWeight: 'bold', color: charcoal.rgb }); + +export type TeamMetric = { + id: string; + name: string; + leadershipRoleCount: number; + previousLeadershipRoleCount: number; + memberCount: number; + previousMemberCount: number; +}; +interface LeadershipMembershipTableProps { + data: TeamMetric[]; +} + +const LeadershipMembershipTable: React.FC = ({ + data, +}) => ( + +
+
+ Team + Currently in a leadership role + Previously in a leadership role + + Currently a member + Previously a member +
+ {data.map((row) => ( +
+ Team +

{row.name}

+ + Currently in a leadership role + +

{row.leadershipRoleCount}

+ + Previously in a leadership role + +

{row.previousLeadershipRoleCount}

+ Currently a member +

{row.memberCount}

+ Previously a member +

{row.previousMemberCount}

+
+ ))} +
+
+); + +export default LeadershipMembershipTable; diff --git a/packages/react-components/src/organisms/MainNavigation.tsx b/packages/react-components/src/organisms/MainNavigation.tsx index 791e6f7d3c..afeb7b1d7d 100644 --- a/packages/react-components/src/organisms/MainNavigation.tsx +++ b/packages/react-components/src/organisms/MainNavigation.tsx @@ -6,7 +6,9 @@ import { sharedResearch, news, events, + analytics, } from '@asap-hub/routing'; +import { isEnabled } from '@asap-hub/flags'; import { perRem, @@ -19,6 +21,7 @@ import { networkIcon, discoverIcon, aboutIcon, + analyticsIcon, LibraryIcon, newsIcon, calendarIcon, @@ -102,6 +105,17 @@ const MainNavigation: React.FC = ({ userOnboarded }) => ( About ASAP + {isEnabled('ANALYTICS') && ( +
  • + + Analytics + +
  • + )} ); diff --git a/packages/react-components/src/organisms/__tests__/LeadershipMembershipTable.test.tsx b/packages/react-components/src/organisms/__tests__/LeadershipMembershipTable.test.tsx new file mode 100644 index 0000000000..0fbd146be8 --- /dev/null +++ b/packages/react-components/src/organisms/__tests__/LeadershipMembershipTable.test.tsx @@ -0,0 +1,19 @@ +import { render } from '@testing-library/react'; +import LeadershipMembershipTable from '../LeadershipMembershipTable'; + +describe('LeadershipMembershipTable', () => { + it('renders data', () => { + const data = [ + { + name: 'Test Team', + id: '1', + leadershipRoleCount: 1, + previousLeadershipRoleCount: 2, + memberCount: 3, + previousMemberCount: 4, + }, + ]; + const { getByText } = render(); + expect(getByText('Test Team')).toBeInTheDocument(); + }); +}); diff --git a/packages/react-components/src/organisms/__tests__/MainNavigation.test.tsx b/packages/react-components/src/organisms/__tests__/MainNavigation.test.tsx index e06a687349..37637a2a26 100644 --- a/packages/react-components/src/organisms/__tests__/MainNavigation.test.tsx +++ b/packages/react-components/src/organisms/__tests__/MainNavigation.test.tsx @@ -19,6 +19,7 @@ it('renders the navigation items', () => { expect.stringMatching(/news/i), expect.stringMatching(/guides/i), expect.stringMatching(/about/i), + expect.stringMatching(/analytics/i), ]); }); diff --git a/packages/react-components/src/organisms/index.ts b/packages/react-components/src/organisms/index.ts index 66ca3d52c0..9ccafe6637 100644 --- a/packages/react-components/src/organisms/index.ts +++ b/packages/react-components/src/organisms/index.ts @@ -24,6 +24,7 @@ export { default as InterestGroupLeadersTabbedCard } from './InterestGroupLeader export { default as InterestGroupList } from './InterestGroupList'; export { default as InterestGroupTeamsTabbedCard } from './InterestGroupTeamsTabbedCard'; export { default as JoinEvent } from './JoinEvent'; +export { default as LeadershipMembershipTable } from './LeadershipMembershipTable'; export { default as MainNavigation } from './MainNavigation'; export { default as MenuHeader } from './MenuHeader'; export { default as NewsCard } from './NewsCard'; diff --git a/packages/react-components/src/templates/AnalyticsPage.tsx b/packages/react-components/src/templates/AnalyticsPage.tsx new file mode 100644 index 0000000000..90eb5e566c --- /dev/null +++ b/packages/react-components/src/templates/AnalyticsPage.tsx @@ -0,0 +1,20 @@ +import { ComponentProps } from 'react'; +import { css } from '@emotion/react'; + +import AnalyticsPageHeader from './AnalyticsPageHeader'; +import { defaultPageLayoutPaddingStyle } from '../layout'; + +const mainStyles = css({ + padding: defaultPageLayoutPaddingStyle, +}); + +type AnalyticsPageProps = ComponentProps; + +const AnalyticsPage: React.FC = ({ children }) => ( +
    + +
    {children}
    +
    +); + +export default AnalyticsPage; diff --git a/packages/react-components/src/templates/AnalyticsPageBody.tsx b/packages/react-components/src/templates/AnalyticsPageBody.tsx new file mode 100644 index 0000000000..a08aa25829 --- /dev/null +++ b/packages/react-components/src/templates/AnalyticsPageBody.tsx @@ -0,0 +1,80 @@ +import { css } from '@emotion/react'; +import { ComponentProps } from 'react'; +import { PageControls } from '..'; +import { Dropdown, Headline3, Paragraph, Subtitle } from '../atoms'; +import { LeadershipMembershipTable } from '../organisms'; +import { perRem } from '../pixels'; + +type MetricOption = 'workingGroup' | 'interestGroup'; +type MetricData = { + id: string; + name: string; + leadershipRoleCount: number; + previousLeadershipRoleCount: number; + memberCount: number; + previousMemberCount: number; +}; + +const metricOptions: Record = { + workingGroup: 'Working Group Leadership & Membership', + interestGroup: 'Interest Group Leadership & Membership', +}; + +const metricOptionList = Object.keys(metricOptions).map((value) => ({ + value: value as MetricOption, + label: metricOptions[value as MetricOption], +})); + +type LeadershipAndMembershipAnalyticsProps = ComponentProps< + typeof PageControls +> & { + metric: MetricOption; + setMetric: (option: MetricOption) => void; + data: MetricData[]; +}; + +const metricDropdownStyles = css({ + marginBottom: `${48 / perRem}em`, +}); + +const tableHeaderStyles = css({ + paddingBottom: `${24 / perRem}em`, +}); + +const pageControlsStyles = css({ + justifySelf: 'center', + paddingTop: `${36 / perRem}em`, + paddingBottom: `${36 / perRem}em`, +}); + +const AnalyticsPageBody: React.FC = ({ + metric, + setMetric, + data, + ...pageControlProps +}) => ( +
    +
    + Metric + +
    +
    + {metricOptions[metric]} + + Teams that are currently or have been previously in a leadership or a + membership role within a Working Group. + +
    + +
    + +
    +
    +); + +export default AnalyticsPageBody; diff --git a/packages/react-components/src/templates/AnalyticsPageHeader.tsx b/packages/react-components/src/templates/AnalyticsPageHeader.tsx new file mode 100644 index 0000000000..8abc6f4b6a --- /dev/null +++ b/packages/react-components/src/templates/AnalyticsPageHeader.tsx @@ -0,0 +1,48 @@ +import { css } from '@emotion/react'; + +import { analytics } from '@asap-hub/routing'; +import { Display, Paragraph, TabLink } from '../atoms'; +import { perRem } from '../pixels'; +import { charcoal, paper, steel } from '../colors'; +import { defaultPageLayoutPaddingStyle } from '../layout'; +import TabNav from '../molecules/TabNav'; +import { LeadershipIcon } from '../icons'; + +const visualHeaderStyles = css({ + padding: `${defaultPageLayoutPaddingStyle} 0`, + background: paper.rgb, + boxShadow: `0 2px 4px -2px ${steel.rgb}`, +}); + +const textStyles = css({ + maxWidth: `${610 / perRem}em`, +}); + +const iconStyles = css({ + display: 'inline-grid', + verticalAlign: 'middle', + paddingRight: `${6 / perRem}em`, +}); + +const AnalyticsPageHeader: React.FC = () => ( +
    +
    + Analytics +
    + + Explore dashboards related to CRN activities. + +
    + + + + + + Leadership & Membership + + +
    +
    +); + +export default AnalyticsPageHeader; diff --git a/packages/react-components/src/templates/__tests__/AnalyticsPage.test.tsx b/packages/react-components/src/templates/__tests__/AnalyticsPage.test.tsx new file mode 100644 index 0000000000..312746f385 --- /dev/null +++ b/packages/react-components/src/templates/__tests__/AnalyticsPage.test.tsx @@ -0,0 +1,13 @@ +import { render } from '@testing-library/react'; + +import AnalyticsPage from '../AnalyticsPage'; + +it('renders the header', () => { + const { getByRole } = render(); + expect(getByRole('heading', { level: 1 })).toHaveTextContent('Analytics'); +}); + +it('renders the children', () => { + const { getByText } = render(Content); + expect(getByText('Content')).toBeVisible(); +}); diff --git a/packages/react-components/src/templates/__tests__/AnalyticsPageBody.test.tsx b/packages/react-components/src/templates/__tests__/AnalyticsPageBody.test.tsx new file mode 100644 index 0000000000..b189d14dce --- /dev/null +++ b/packages/react-components/src/templates/__tests__/AnalyticsPageBody.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from '@testing-library/react'; + +import AnalyticsPageBody from '../AnalyticsPageBody'; + +const pageControlProps = { + numberOfPages: 1, + numberOfItems: 3, + currentPageIndex: 0, + renderPageHref: () => '', +}; +it('renders the selected metric', () => { + render( + {}} + {...pageControlProps} + />, + ); + expect( + screen.getByText(/Interest Group Leadership & Membership/, { + selector: 'h3', + }), + ).toBeVisible(); +}); diff --git a/packages/react-components/src/templates/__tests__/AnalyticsPageHeader.test.tsx b/packages/react-components/src/templates/__tests__/AnalyticsPageHeader.test.tsx new file mode 100644 index 0000000000..f819a2eb2c --- /dev/null +++ b/packages/react-components/src/templates/__tests__/AnalyticsPageHeader.test.tsx @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; + +import AnalyticsPageHeader from '../AnalyticsPageHeader'; + +it('renders the header', () => { + render(); + expect(screen.getByText(/Analytics/, { selector: 'h1' })).toBeVisible(); +}); diff --git a/packages/react-components/src/templates/index.ts b/packages/react-components/src/templates/index.ts index d189941dc6..7861a5b82d 100644 --- a/packages/react-components/src/templates/index.ts +++ b/packages/react-components/src/templates/index.ts @@ -1,6 +1,9 @@ export { default as AboutPage } from './AboutPage'; export { default as AboutPageBody } from './AboutPageBody'; export { default as AboutPageHeader } from './AboutPageHeader'; +export { default as AnalyticsPage } from './AnalyticsPage'; +export { default as AnalyticsPageBody } from './AnalyticsPageBody'; +export { default as AnalyticsPageHeader } from './AnalyticsPageHeader'; export { default as BasicLayout } from './BasicLayout'; export { default as BiographyModal } from './BiographyModal'; export { default as ContactInfoModal } from './ContactInfoModal'; diff --git a/packages/routing/src/analytics.ts b/packages/routing/src/analytics.ts new file mode 100644 index 0000000000..6f401ca77f --- /dev/null +++ b/packages/routing/src/analytics.ts @@ -0,0 +1,3 @@ +import { route } from 'typesafe-routes'; + +export default route('/analytics', {}, {}); diff --git a/packages/routing/src/index.ts b/packages/routing/src/index.ts index 145f59ec26..3376ce0c99 100644 --- a/packages/routing/src/index.ts +++ b/packages/routing/src/index.ts @@ -4,6 +4,7 @@ import { RouteNode } from 'typesafe-routes'; import * as gp2 from './gp2'; export { default as about } from './about'; +export { default as analytics } from './analytics'; export { default as discover } from './discover'; export { default as events } from './events'; export { default as logout } from './logout';