Skip to content

Commit

Permalink
Page footers (#291)
Browse files Browse the repository at this point in the history
  • Loading branch information
macfarlandian authored Jan 12, 2021
1 parent 3a6397c commit 69d222f
Show file tree
Hide file tree
Showing 35 changed files with 596 additions and 76 deletions.
18 changes: 18 additions & 0 deletions spotlight-client/@types/react-stickyfill.d.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/>.
// =============================================================================

declare module "react-stickyfill";
1 change: 1 addition & 0 deletions spotlight-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"react-is": "^16.13.1",
"react-scripts": "3.4.3",
"react-spring": "^8.0.27",
"react-stickyfill": "^0.2.5",
"styled-components": "^5.2.1",
"styled-reset": "^4.3.3",
"typescript": "^4.0.0",
Expand Down
5 changes: 4 additions & 1 deletion spotlight-client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import React from "react";
import { HelmetProvider } from "react-helmet-async";
import styled from "styled-components/macro";
import AuthWall from "./AuthWall";
import { NAV_BAR_HEIGHT } from "./constants";
import { FOOTER_HEIGHT, NAV_BAR_HEIGHT } from "./constants";
import GlobalStyles from "./GlobalStyles";
import PageHome from "./PageHome";
import PageNarrative from "./PageNarrative";
import PageNarrativeList from "./PageNarrativeList";
import PageNotFound from "./PageNotFound";
import PageTenant from "./PageTenant";
import { NarrativesSlug } from "./routerUtils/types";
import SiteFooter from "./SiteFooter";
import SiteNavigation from "./SiteNavigation";
import StoreProvider from "./StoreProvider";

Expand All @@ -41,6 +42,7 @@ const PassThroughPage: React.FC<RouteComponentProps> = ({ children }) => (

const Main = styled.div.attrs((props) => ({ role: "main" }))`
margin-top: ${rem(NAV_BAR_HEIGHT)};
min-height: calc(100vh - ${rem(NAV_BAR_HEIGHT + FOOTER_HEIGHT)});
`;

const App: React.FC = () => {
Expand Down Expand Up @@ -68,6 +70,7 @@ const App: React.FC = () => {
<PageNotFound default />
</Router>
</Main>
<SiteFooter />
</AuthWall>
</StoreProvider>
</HelmetProvider>
Expand Down
22 changes: 21 additions & 1 deletion spotlight-client/src/DataStore/DataStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

import { runInAction } from "mobx";
import SystemNarrative from "../contentModels/SystemNarrative";
import Tenant from "../contentModels/Tenant";
import { reactImmediately } from "../testUtils";
import RootStore from "./RootStore";
Expand Down Expand Up @@ -44,10 +46,28 @@ describe("tenant store", () => {
});

test("can set current tenant", () => {
tenantStore.setCurrentTenant({ tenantId: "US_ND" });
runInAction(() => {
tenantStore.currentTenantId = "US_ND";
});

reactImmediately(() => {
expect(tenantStore.currentTenant).toBeInstanceOf(Tenant);
});
expect.hasAssertions();
});

test("can set current narrative", () => {
expect(tenantStore.currentTenant).toBeUndefined();

runInAction(() => {
tenantStore.currentTenantId = "US_ND";
tenantStore.currentNarrativeTypeId = "Prison";
});

reactImmediately(() => {
expect(tenantStore.currentNarrative).toBeInstanceOf(SystemNarrative);
});

expect.assertions(2);
});
});
4 changes: 4 additions & 0 deletions spotlight-client/src/DataStore/RootStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,8 @@ export default class RootStore {
get tenant(): TenantStore["currentTenant"] {
return this.tenantStore.currentTenant;
}

get narrative(): TenantStore["currentNarrative"] {
return this.tenantStore.currentNarrative;
}
}
34 changes: 27 additions & 7 deletions spotlight-client/src/DataStore/TenantStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,46 @@
// =============================================================================

import { makeAutoObservable } from "mobx";
import { TenantId } from "../contentApi/types";
import { SystemNarrativeTypeId, TenantId } from "../contentApi/types";
import type SystemNarrative from "../contentModels/SystemNarrative";
import Tenant, { createTenant } from "../contentModels/Tenant";
import type RootStore from "./RootStore";

export default class TenantStore {
currentTenant?: Tenant;
currentNarrativeTypeId?: SystemNarrativeTypeId;

currentTenantId?: TenantId;

rootStore: RootStore;

tenants: Map<TenantId, Tenant>;

constructor({ rootStore }: { rootStore: RootStore }) {
makeAutoObservable(this, { rootStore: false });

this.rootStore = rootStore;

this.tenants = new Map();
}

setCurrentTenant({ tenantId }: { tenantId: TenantId | undefined }): void {
if (!tenantId) {
this.currentTenant = undefined;
} else if (tenantId !== this.currentTenant?.id) {
this.currentTenant = createTenant({ tenantId });
/**
* Retrieves the current tenant from the mapping of available tenants,
* as indicated by this.currentTenantId.
* Creates the Tenant on demand if it does not yet exist.
*/
get currentTenant(): Tenant | undefined {
if (!this.currentTenantId) return undefined;
if (!this.tenants.has(this.currentTenantId)) {
this.tenants.set(
this.currentTenantId,
createTenant({ tenantId: this.currentTenantId })
);
}
return this.tenants.get(this.currentTenantId);
}

get currentNarrative(): SystemNarrative | undefined {
if (!this.currentNarrativeTypeId || !this.currentTenant) return undefined;
return this.currentTenant.systemNarratives[this.currentNarrativeTypeId];
}
}
2 changes: 1 addition & 1 deletion spotlight-client/src/GlobalStyles/GlobalStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const GlobalStyles: React.FC = () => {
<Helmet>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Libre+Franklin&display=swap"
href="https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@400;700&display=swap"
rel="stylesheet"
/>
<link
Expand Down
142 changes: 142 additions & 0 deletions spotlight-client/src/NarrativeFooter/NarrativeFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// 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 { TenantId } from "../contentApi/types";
import SystemNarrative from "../contentModels/SystemNarrative";
import getUrlForResource from "../routerUtils/getUrlForResource";
import { useDataStore } from "../StoreProvider";
import { colors, typefaces } from "../UiLibrary";
import Arrow from "../UiLibrary/Arrow";

const Container = styled.nav`
padding: ${rem(120)} ${rem(32)};
.NarrativeFooter__BackLink {
color: ${colors.link};
display: inline-block;
font-weight: 500;
font-size: ${rem(18)};
line-height: 1.7;
margin-top: ${rem(48)};
text-decoration: none;
}
`;

const Heading = styled.h2`
font-family: ${typefaces.display};
font-size: ${rem(32)};
line-height: 1.75;
letter-spacing: -0.04em;
`;

const LinkList = styled.ul`
display: flex;
font-size: ${rem(24)};
line-height: 1.5;
margin: ${rem(48)} -${rem(16)};
`;

const LinkListItem = styled.li`
border-top: 1px solid ${colors.rule};
flex: 1 1 auto;
margin: 0 ${rem(16)};
a {
color: ${colors.text};
display: block;
padding-top: ${rem(32)};
text-decoration: none;
width: 100%;
}
`;

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

return (
<LinkListItem>
<Link
to={getUrlForResource({
page: "narrative",
params: { tenantId, narrativeTypeId: narrative.id },
})}
onMouseOver={() => setAnimationStyles({ opacity: 1 })}
onFocus={() => setAnimationStyles({ opacity: 1 })}
onMouseOut={() => setAnimationStyles({ opacity: 0 })}
onBlur={() => setAnimationStyles({ opacity: 0 })}
>
{narrative.title}{" "}
<animated.span style={animationStyles}>
<Arrow color={colors.link} direction="right" />
</animated.span>
</Link>
</LinkListItem>
);
};

const Footer: React.FC = () => {
const {
tenant,
tenantStore: { currentNarrativeTypeId },
} = useDataStore();

if (!tenant) return null;

const narrativesToDisplay = Object.values(tenant.systemNarratives).filter(
(narrative) => narrative && narrative.id !== currentNarrativeTypeId
) as SystemNarrative[]; // this assertion is safe because undefined items were filtered out

return (
<Container aria-label="collections">
<Heading>Continue Reading</Heading>
<LinkList>
{narrativesToDisplay.map((narrative) => {
return (
<FooterLink
key={narrative.id}
tenantId={tenant.id}
narrative={narrative}
/>
);
})}
</LinkList>
<Link
className="NarrativeFooter__BackLink"
to={getUrlForResource({
page: "narrative list",
params: { tenantId: tenant.id },
})}
>
<Arrow direction="left" /> Back to Collections
</Link>
</Container>
);
};

export default observer(Footer);
18 changes: 18 additions & 0 deletions spotlight-client/src/NarrativeFooter/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 "./NarrativeFooter";
30 changes: 9 additions & 21 deletions spotlight-client/src/PageNarrative/PageNarrative.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,23 @@
// =============================================================================

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 NarrativeFooter from "../NarrativeFooter";
import SystemNarrativePage from "../SystemNarrativePage";
import withRouteSync from "../withRouteSync";

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

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

// 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];

if (narrative) {
return <SystemNarrativePage narrative={narrative} />;
}

return null;
const PageNarrative: React.FC<PageNarrativeProps> = () => {
return (
<>
<SystemNarrativePage />
<NarrativeFooter />
</>
);
};

export default withRouteSync(observer(PageNarrative));
export default withRouteSync(PageNarrative);
Loading

0 comments on commit 69d222f

Please sign in to comment.