Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp Spotlight state landing page #477

Merged
merged 15 commits into from
Sep 13, 2021
20 changes: 6 additions & 14 deletions spotlight-client/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe("navigation", () => {
test("tenant home", () => {
expect.hasAssertions();
const targetPath = "/us-nd";
const lookupArgs = ["heading", { name: /North Dakota/, level: 1 }] as const;
const lookupArgs = ["heading", { name: /DOCR/, level: 1 }] as const;

return verifyWithNavigation({ targetPath, lookupArgs });
});
Expand Down Expand Up @@ -128,7 +128,7 @@ describe("navigation", () => {
const homeLink = inNav.getByRole("link", { name: "Spotlight" });
const tenantLink = inNav.getByRole("link", { name: "North Dakota" });
const sentencingLink = await screen.findByRole("link", {
name: "Sentencing",
name: "Sentencing Data",
});

fireEvent.click(sentencingLink);
Expand All @@ -142,20 +142,12 @@ describe("navigation", () => {

fireEvent.click(tenantLink);
await waitFor(async () =>
expect(await screen.findByTestId("PageTitle")).toHaveTextContent(
"Explore correctional data from North Dakota."
)
expect(await screen.findByTestId("PageTitle")).toHaveTextContent("DOCR")
);

const disparitiesLink = screen.getByRole("link", {
name: "Racial Disparities",
});
fireEvent.click(disparitiesLink);
await waitFor(async () =>
expect(await screen.findByTestId("PageTitle")).toHaveTextContent(
"Racial Disparities"
)
);
expect(
screen.queryByText("Racial Disparities Data")
).not.toBeInTheDocument();

fireEvent.click(homeLink);
await waitFor(async () =>
Expand Down
20 changes: 14 additions & 6 deletions spotlight-client/src/MetricVizMapper/MetricVizMapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,29 @@ import ProgramParticipationCurrentMetric from "../contentModels/ProgramParticipa
import VizProgramParticipationCurrent from "../VizProgramParticipationCurrent";

type MetricVizMapperProps = {
metric: Metric<MetricRecord>;
metric: Metric<MetricRecord> | undefined;
preview?: boolean;
};

const MetricVizMapper: React.FC<MetricVizMapperProps> = ({ metric }) => {
const MetricVizMapper: React.FC<MetricVizMapperProps> = ({
metric,
preview,
}) => {
if (metric instanceof HistoricalPopulationBreakdownMetric) {
return <VizHistoricalPopulationBreakdown metric={metric} />;
return (
<VizHistoricalPopulationBreakdown preview={preview} metric={metric} />
);
}
if (metric instanceof PopulationBreakdownByLocationMetric) {
return <VizPopulationBreakdownByLocation metric={metric} />;
return (
<VizPopulationBreakdownByLocation preview={preview} metric={metric} />
);
}
if (metric instanceof DemographicsByCategoryMetric) {
if (metric.id === "PrisonStayLengthAggregate") {
return <VizPrisonStayLengths metric={metric} />;
}
return <VizDemographicsByCategory metric={metric} />;
return <VizDemographicsByCategory preview={preview} metric={metric} />;
}
if (metric instanceof RecidivismRateMetric) {
if (metric.id === "PrisonRecidivismRateSingleFollowupHistorical") {
Expand All @@ -65,7 +73,7 @@ const MetricVizMapper: React.FC<MetricVizMapperProps> = ({ metric }) => {
return <VizSupervisionSuccessRate metric={metric} />;
}
if (metric instanceof ProgramParticipationCurrentMetric) {
return <VizProgramParticipationCurrent metric={metric} />;
return <VizProgramParticipationCurrent preview={preview} metric={metric} />;
}

// there are no other metric types, so this should only be reached when developing new ones
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,15 @@ const LinkListItem = styled.li`
white-space: nowrap;
/* use width to create 1-4 columns, depending on screen size */
width: 100%;

@media (min-width: ${breakpoints.tablet[0]}px) {
width: calc(100% / 2);
}

@media (min-width: ${breakpoints.desktop[0]}px) {
width: calc(100% / 3);
}

@media (min-width: ${breakpoints.xl[0]}px) {
width: calc(100% / 4);
}

a {
border-top: 1px solid ${colors.rule};
color: ${colors.text};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2021 Recidiviz, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

import { Link } from "@reach/router";
import { observer } from "mobx-react-lite";
import { rem } from "polished";
import React from "react";
import { animated, useSpring } from "react-spring/web.cjs";
import styled from "styled-components/macro";
import { track } from "../analytics";
import { TenantId } from "../contentApi/types";
import SystemNarrative from "../contentModels/SystemNarrative";
import MetricVizMapper from "../MetricVizMapper";
import getUrlForResource from "../routerUtils/getUrlForResource";
import { useDataStore } from "../StoreProvider";
import { breakpoints, colors } from "../UiLibrary";
import Arrow from "../UiLibrary/Arrow";

// grid styles adapted from IE-safe auto placement grid
// https://css-tricks.com/css-grid-in-ie-faking-an-auto-placement-grid-with-gaps/

const Wrapper = styled.div`
/* prevents the trailing grid gaps from pushing other stuff around */
@media (max-width: ${breakpoints.tablet[0]}px) {
overflow: hidden;
}
`;

const LinkList = styled.ul`
display: flex;
flex-wrap: wrap;
font-size: ${rem(24)};
line-height: 1.5;
/* this margin makes the cells flush left and right */
margin: ${rem(48)} -${rem(32)} 0 0;
`;

const LinkListItem = styled.li`
/* creates gaps */
border: 0 solid transparent;
border-width: 0 ${rem(32)} 0 0;
flex: 0 0 auto;
white-space: nowrap;
/* use width to create 1-4 columns, depending on screen size */
width: 100%;

@media (min-width: ${breakpoints.tablet[0]}px) {
width: calc(100% / 1);
}

@media (min-width: ${breakpoints.desktop[0]}px) {
width: calc(100% / 1);
}

@media (min-width: ${breakpoints.xl[0]}px) {
width: calc(100% / 2);
}

a {
border-top: 1px solid ${colors.rule};
color: ${colors.text};
display: block;
padding-right: ${rem(8)};
padding-top: ${rem(24)};
padding-bottom: ${rem(24)};
text-decoration: none;
width: 100%;
}

a:not(:last-child) {
border-bottom: 1px solid ${colors.rule};
}
`;

const LinkText = styled.span`
white-space: normal;
`;

const ChartTitle = styled.span`
font-size: ${rem(16)};
`;

const ChartPreview = styled.div`
padding-top: ${rem(16)};
`;

const ChartPreviewComponent: React.FC<{
narrative: SystemNarrative;
}> = ({ narrative }) => {
if (narrative.preview)
return (
<>
<ChartTitle>{narrative.previewTitle}</ChartTitle>
<ChartPreview>
<MetricVizMapper
preview
metric={
narrative.sections.find(
(section) => section.metric.id === narrative.preview
)?.metric
}
/>
</ChartPreview>
</>
);
return null;
};

const NarrativeLink: React.FC<{
narrative: SystemNarrative;
tenantId: TenantId;
}> = observer(({ narrative, tenantId }) => {
const [animationStyles, setAnimationStyles] = useSpring(() => ({
opacity: 0,
from: { opacity: 0 },
}));

return (
<LinkListItem>
<Link
to={getUrlForResource({
page: "narrative",
params: { tenantId, narrativeTypeId: narrative.id },
})}
onClick={() =>
track("narrative_body_link_clicked", {
category: "navigation",
label: narrative.id,
})
}
onMouseOver={() => setAnimationStyles({ opacity: 1 })}
onFocus={() => setAnimationStyles({ opacity: 1 })}
onMouseOut={() => setAnimationStyles({ opacity: 0 })}
onBlur={() => setAnimationStyles({ opacity: 0 })}
>
<LinkText>{narrative.title} Data</LinkText>&nbsp;
<animated.span style={animationStyles}>
<Arrow color={colors.link} direction="right" />
</animated.span>
</Link>
<ChartPreviewComponent narrative={narrative} />
</LinkListItem>
);
});

/**
* Produces a grid of links to available narratives for the current tenant.
* If there is a current narrative selected, it will be excluded from the grid.
*/
const OtherNarrativeLinksPreview = (): React.ReactElement | null => {
const {
tenant,
tenantStore: { currentNarrativeTypeId },
} = useDataStore();

if (!tenant) return null;

const narrativesToDisplay = [
...Object.values(tenant.systemNarratives),
].filter((narrative): narrative is SystemNarrative => {
if (narrative === undefined) return false;
return narrative.id !== currentNarrativeTypeId;
});

return (
<Wrapper>
<LinkList>
{narrativesToDisplay.map((narrative) => {
return (
<NarrativeLink
key={narrative.id}
tenantId={tenant.id}
narrative={narrative}
/>
);
})}
</LinkList>
</Wrapper>
);
};

export default observer(OtherNarrativeLinksPreview);
18 changes: 18 additions & 0 deletions spotlight-client/src/OtherNarrativeLinksPreview/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2021 Recidiviz, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

export { default } from "./OtherNarrativeLinksPreview";
19 changes: 7 additions & 12 deletions spotlight-client/src/PageTenant/PageTenant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import { rem } from "polished";
import React from "react";
import styled from "styled-components/macro";
import { NAV_BAR_HEIGHT } from "../constants";
import OtherNarrativeLinks from "../OtherNarrativeLinks";
import OtherNarrativeLinksPreview from "../OtherNarrativeLinksPreview";
import { useDataStore } from "../StoreProvider";
import { breakpoints, CopyBlock, PageSection, PageTitle } from "../UiLibrary";
import { breakpoints, PageSection, PageTitle } from "../UiLibrary";
import withRouteSync from "../withRouteSync";

const Introduction = styled(PageSection)`
Expand All @@ -35,6 +35,7 @@ const Introduction = styled(PageSection)`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
/* try to keep the links "above the fold" */
min-height: calc(100vh - ${rem(NAV_BAR_HEIGHT)} - ${rem(130)});

Expand All @@ -48,13 +49,8 @@ const Introduction = styled(PageSection)`
const Links = styled(PageSection)``;

const Title = styled(PageTitle)`
max-width: ${rem(760)};
`;

const Description = styled(CopyBlock)`
font-size: ${rem(20)};
line-height: 1.7;
max-width: ${rem(760)};
font-size: ${rem(40)};
max-width: ${rem(1100)};
`;

const PageTenant: React.FC<RouteComponentProps> = () => {
Expand All @@ -66,11 +62,10 @@ const PageTenant: React.FC<RouteComponentProps> = () => {
// tenant may be briefly undefined during initial page load
<article>
<Introduction>
<Title>{tenant.landingPageTitle}</Title>
macfarlandian marked this conversation as resolved.
Show resolved Hide resolved
<Description>{HTMLReactParser(tenant.description)}</Description>
<Title>{HTMLReactParser(tenant.description)}</Title>
</Introduction>
<Links>
<OtherNarrativeLinks />
<OtherNarrativeLinksPreview />
</Links>
</article>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { TenantContent } from "../../contentApi/types";

const content: TenantContent = {
name: "Test Tenant",
landingPageTitle: "test landing page title",
description: "test tenant description",
coBrandingCopy: "test tenant co-branding",
feedbackUrl: "https://example.com/feedback",
Expand Down Expand Up @@ -47,6 +46,8 @@ const content: TenantContent = {
systemNarratives: {
Parole: {
title: "test parole narrative",
previewTitle: "test parole subtitle",
preview: "ParolePopulationCurrent",
introduction: "test parole introduction",
sections: [
{
Expand All @@ -63,6 +64,8 @@ const content: TenantContent = {
},
Sentencing: {
title: "test sentencing narrative",
previewTitle: "test sentencing subtitle",
preview: "SentencePopulationCurrent",
introduction:
'test sentencing introduction <a href="https://example.com">intro link</a>',
sections: [
Expand Down
Loading