Skip to content

Commit

Permalink
add narratives to router and UI
Browse files Browse the repository at this point in the history
  • Loading branch information
macfarlandian committed Dec 19, 2020
1 parent 6ea16b2 commit 9bf739a
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 30 deletions.
14 changes: 14 additions & 0 deletions spotlight-client/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@ describe("navigation", () => {
return verifyWithNavigation({ targetPath, lookupArgs });
});

test("single narrative page", () => {
expect.hasAssertions();
const targetPath = "/us-nd/narratives/prison";
const lookupArgs = [
"heading",
{
name: testContent.systemNarratives.Prison?.title,
level: 1,
},
] as const;

return verifyWithNavigation({ targetPath, lookupArgs });
});

test("nav bar", async () => {
const dataPortalLabel = "Explore";
const narrativesLabel = "Collections";
Expand Down
6 changes: 4 additions & 2 deletions spotlight-client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ import GlobalStyles from "./GlobalStyles";
import PageExplore from "./PageExplore";
import PageHome from "./PageHome";
import PageMetric from "./PageMetric";
import PageNarrativeHome from "./PageNarrativeHome";
import PageNarrative from "./PageNarrative";
import PageNarrativeList from "./PageNarrativeList";
import PageNotFound from "./PageNotFound";
import PageTenant from "./PageTenant";
import { DataPortalSlug, NarrativesSlug } from "./routerUtils/types";
Expand Down Expand Up @@ -59,7 +60,8 @@ const App: React.FC = () => {
<PageNotFound default />
</PassThroughPage>
<PassThroughPage path={`/${NarrativesSlug}`}>
<PageNarrativeHome path="/" />
<PageNarrativeList path="/" />
<PageNarrative path="/:narrativeTypeId" />
</PassThroughPage>
<PageNotFound default />
</PassThroughPage>
Expand Down
50 changes: 50 additions & 0 deletions spotlight-client/src/PageNarrative/PageNarrative.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 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 { RouteComponentProps } from "@reach/router";
import { observer } from "mobx-react-lite";
import React from "react";
import { SystemNarrativeTypeId } from "../contentApi/types";
import { useDataStore } from "../StoreProvider";
import withRouteSync from "../withRouteSync";

type PageNarrativeProps = RouteComponentProps & {
narrativeTypeId?: SystemNarrativeTypeId;
};

const PageNarrative: React.FC<PageNarrativeProps> = ({ narrativeTypeId }) => {
const tenant = useDataStore().tenantStore.currentTenant;

// if this component is used properly as a route component,
// this should never be true;
// if it is, something has gone very wrong
if (!narrativeTypeId) {
throw new Error("missing narrativeTypeId");
}

// tenant may be briefly undefined on initial page load
const narrative = tenant?.systemNarratives[narrativeTypeId];

return (
<article>
<h1>{narrative?.title}</h1>
<p>{narrative?.introduction}</p>
</article>
);
};

export default withRouteSync(observer(PageNarrative));
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

export { default } from "./PageNarrativeHome";
export { default } from "./PageNarrative";
62 changes: 62 additions & 0 deletions spotlight-client/src/PageNarrativeList/PageNarrativeList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 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, RouteComponentProps } from "@reach/router";
import { observer } from "mobx-react-lite";
import React from "react";
import { SystemNarrativeTypeIdList } from "../contentApi/types";
import getUrlForResource from "../routerUtils/getUrlForResource";
import { useDataStore } from "../StoreProvider";
import withRouteSync from "../withRouteSync";

const PageNarrativeList: React.FC<RouteComponentProps> = () => {
const tenant = useDataStore().tenantStore.currentTenant;

const systemNarratives = tenant?.systemNarratives;

return (
<article>
<h1>Collections</h1>
{tenant && systemNarratives && Object.keys(systemNarratives).length > 0 && (
<section>
<h2>system overview</h2>
<ul>
{SystemNarrativeTypeIdList.map((id) => {
const narrative = systemNarratives[id];
return (
narrative && (
<li key={id}>
<Link
to={getUrlForResource({
page: "narrative",
params: { tenantId: tenant.id, narrativeTypeId: id },
})}
>
{narrative.title}
</Link>
</li>
)
);
})}
</ul>
</section>
)}
</article>
);
};

export default withRouteSync(observer(PageNarrativeList));
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,4 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

import { RouteComponentProps } from "@reach/router";
import React from "react";
import withRouteSync from "../withRouteSync";

const PageNarrativeHome: React.FC<RouteComponentProps> = () => (
<article>
<h1>Collections</h1>
</article>
);

export default withRouteSync(PageNarrativeHome);
export { default } from "./PageNarrativeList";
2 changes: 1 addition & 1 deletion spotlight-client/src/SiteNavigation/SiteNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ const SiteNavigation: React.FC = () => {
<Link
getProps={getNavLinkProps({ matchPartial: true })}
to={getUrlForResource({
page: "narratives",
page: "narrative list",
params: { tenantId: tenant.id },
})}
>
Expand Down
13 changes: 10 additions & 3 deletions spotlight-client/src/routerUtils/getUrlForResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ type GetUrlOptions =
page: "metric";
params: Pick<RequiredParams, "tenantId" | "metricTypeId">;
}
| { page: "narratives"; params: Pick<RequiredParams, "tenantId"> };
| { page: "narrative list"; params: Pick<RequiredParams, "tenantId"> }
| {
page: "narrative";
params: Pick<RequiredParams, "tenantId" | "narrativeTypeId">;
};

/**
* Creates a properly parameterized URL from the input options
Expand All @@ -50,9 +54,12 @@ function getUrlForResource(opts: GetUrlOptions): string {
return `/${makeRouteParam(
opts.params.tenantId
)}/${DataPortalSlug}/${makeRouteParam(opts.params.metricTypeId)}`;
case "narratives":
case "narrative list":
return `/${makeRouteParam(opts.params.tenantId)}/${NarrativesSlug}`;

case "narrative":
return `/${makeRouteParam(
opts.params.tenantId
)}/${NarrativesSlug}/${makeRouteParam(opts.params.narrativeTypeId)}`;
default:
assertNever(opts);
}
Expand Down
34 changes: 23 additions & 11 deletions spotlight-client/src/routerUtils/normalizeRouteParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
// =============================================================================

import { constantCase, pascalCase } from "change-case";
import { isMetricTypeId, isTenantId } from "../contentApi/types";
import { ValuesType } from "utility-types";
import {
isMetricTypeId,
isSystemNarrativeTypeId,
isTenantId,
} from "../contentApi/types";
import { NormalizedRouteParams, RouteParams } from "./types";

/**
Expand All @@ -25,18 +30,16 @@ import { NormalizedRouteParams, RouteParams } from "./types";
export default function normalizeRouteParams(
rawParams: RouteParams
): NormalizedRouteParams {
if (rawParams && typeof rawParams === "object") {
const { tenantId, metricTypeId } = rawParams as { [key: string]: unknown };
const { tenantId, metricTypeId, narrativeTypeId } = rawParams;

return {
tenantId: normalizeTenantId(tenantId),
metricTypeId: normalizeMetricTypeId(metricTypeId),
};
}
return {};
return {
tenantId: normalizeTenantId(tenantId),
metricTypeId: normalizeMetricTypeId(metricTypeId),
narrativeTypeId: normalizeNarrativeTypeId(narrativeTypeId),
};
}

function normalizeTenantId(rawParam: unknown) {
function normalizeTenantId(rawParam: ValuesType<RouteParams>) {
if (typeof rawParam === "string") {
const normalizedString = constantCase(rawParam);
if (isTenantId(normalizedString)) return normalizedString;
Expand All @@ -45,11 +48,20 @@ function normalizeTenantId(rawParam: unknown) {
return undefined;
}

function normalizeMetricTypeId(rawParam: unknown) {
function normalizeMetricTypeId(rawParam: ValuesType<RouteParams>) {
if (typeof rawParam === "string") {
const normalizedString = pascalCase(rawParam);
if (isMetricTypeId(normalizedString)) return normalizedString;
throw new Error(`unknown MetricTypeId: ${normalizedString}`);
}
return undefined;
}

function normalizeNarrativeTypeId(rawParam: ValuesType<RouteParams>) {
if (typeof rawParam === "string") {
const normalizedString = pascalCase(rawParam);
if (isSystemNarrativeTypeId(normalizedString)) return normalizedString;
throw new Error(`unknown narrative type id: ${normalizedString}`);
}
return undefined;
}
8 changes: 7 additions & 1 deletion spotlight-client/src/routerUtils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,23 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

import { MetricTypeId, TenantId } from "../contentApi/types";
import {
MetricTypeId,
SystemNarrativeTypeId,
TenantId,
} from "../contentApi/types";

export type RouteParams = {
// these should match paths as defined in App.tsx
tenantId?: string;
metricTypeId?: string;
narrativeTypeId?: string;
};

export type NormalizedRouteParams = {
tenantId?: TenantId;
metricTypeId?: MetricTypeId;
narrativeTypeId?: SystemNarrativeTypeId;
};

export const DataPortalSlug = "explore";
Expand Down
1 change: 1 addition & 0 deletions spotlight-client/src/withRouteSync/withRouteSync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const withRouteSync = <Props extends RouteComponentProps & RouteParams>(
{...props}
tenantId={normalizedProps.tenantId}
metricTypeId={normalizedProps.metricTypeId}
narrativeTypeId={normalizedProps.narrativeTypeId}
/>
);
};
Expand Down

0 comments on commit 9bf739a

Please sign in to comment.