Skip to content

Commit c7738fb

Browse files
authored
Merge pull request #518 from CivicDataLab/heat-implementation
Heat implementation
2 parents 115c593 + c8b9786 commit c7738fb

86 files changed

Lines changed: 5068 additions & 1745 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React, { cache } from 'react';
2+
import { notFound } from 'next/navigation';
3+
4+
import { AnalyticsSideBarLayout } from '@/components/analytics/analytics-sidebar-layout';
5+
import { states } from '@/config/site';
6+
import { fetchStatesList } from '@/lib/analytics/fetch-states-list';
7+
import { isValidModuleForState } from '@/lib/analytics/module-config';
8+
9+
const getStatesList = cache(async (moduleSlug: string) =>
10+
fetchStatesList(moduleSlug)
11+
);
12+
13+
export default async function ModuleAnalyticsLayout({
14+
children,
15+
params,
16+
}: {
17+
children: React.ReactNode;
18+
params: Promise<{ state: string; module: string }>;
19+
}) {
20+
const { state, module } = await params;
21+
22+
const statesListData = await getStatesList(module);
23+
24+
const isActiveState = (stateSlug: string) =>
25+
states.some(
26+
(item) => item.slug === stateSlug && item.status === 'active'
27+
);
28+
const availableStates = statesListData.filter((item) =>
29+
isActiveState(item.slug)
30+
);
31+
const currentState = statesListData?.find(
32+
(item) => item.slug === state && isActiveState(item.slug)
33+
);
34+
35+
if (!currentState) notFound();
36+
// The backend's module list for this state is authoritative.
37+
if (!isValidModuleForState(currentState.modules, module)) notFound();
38+
39+
return (
40+
<AnalyticsSideBarLayout
41+
currentState={currentState}
42+
statesList={availableStates}
43+
>
44+
{children}
45+
</AnalyticsSideBarLayout>
46+
);
47+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
2+
import { notFound } from 'next/navigation';
3+
4+
import { AnalyticsMainLayout } from '@/components/analytics/analytics-layout';
5+
import { fetchStatesList } from '@/lib/analytics/fetch-states-list';
6+
import { isValidModuleForState } from '@/lib/analytics/module-config';
7+
import { prefetchAnalyticsPageData } from '@/lib/analytics/prefetch';
8+
9+
export default async function ModuleAnalyticsPage({
10+
params,
11+
searchParams,
12+
}: {
13+
params: Promise<{ state: string; module: string }>;
14+
searchParams: Promise<{ [key: string]: string }>;
15+
}) {
16+
const { state: stateSlug, module } = await params;
17+
const { indicator } = await searchParams;
18+
19+
// Validate against the backend's authoritative module list (cached, so this
20+
// reuses the same fetch the layout already issued for this request).
21+
const statesList = await fetchStatesList(module);
22+
const currentState = statesList?.find((item) => item.slug === stateSlug);
23+
if (!currentState || !isValidModuleForState(currentState.modules, module)) {
24+
notFound();
25+
}
26+
27+
const queryClient = await prefetchAnalyticsPageData({
28+
stateSlug,
29+
moduleSlug: module,
30+
indicator,
31+
});
32+
33+
return (
34+
<HydrationBoundary state={dehydrate(queryClient)}>
35+
<AnalyticsMainLayout />
36+
</HydrationBoundary>
37+
);
38+
}

app/[locale]/[state]/analytics/layout.tsx

Lines changed: 0 additions & 70 deletions
This file was deleted.

app/[locale]/[state]/analytics/page.tsx

Lines changed: 0 additions & 60 deletions
This file was deleted.

app/[locale]/[state]/page.tsx

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
'use client';
2+
3+
import Image from 'next/image';
4+
import Link from 'next/link';
5+
import { useParams } from 'next/navigation';
6+
import { useModuleHubStats } from '@/hooks/use-module-hub-stats';
7+
import { useStateName } from '@/hooks/use-state-name';
8+
import { IconArrowRight, IconInfoCircle } from '@tabler/icons-react';
9+
import type { Module } from 'ids-drr-branding-types';
10+
import { useTranslations } from 'next-intl';
11+
import { Tag, Text } from 'opub-ui';
12+
13+
import { states } from '@/config/site';
14+
import { routes } from '@/lib/routes';
15+
import NotFound from '../not-found';
16+
17+
function ModuleHubCard({
18+
stateSlug,
19+
hazardModule,
20+
}: {
21+
stateSlug: string;
22+
hazardModule: Module;
23+
}) {
24+
const t = useTranslations('analytics.hub');
25+
const tCommon = useTranslations('common');
26+
const { districtCount, isLoading, veryHighRiskCount } = useModuleHubStats(
27+
stateSlug,
28+
hazardModule.slug
29+
);
30+
31+
return (
32+
<div className="flex min-w-0 flex-col gap-4 overflow-hidden rounded-2 bg-surfaceDefault p-8 shadow-basicMd md:flex-row ">
33+
<Image
34+
src={hazardModule.icon}
35+
alt=""
36+
className=" mx-auto h-20 w-20 shrink-0 object-contain sm:h-20 sm:w-20 md:mx-0 md:h-20 md:w-20"
37+
/>
38+
<div className="flex min-w-0 flex-1 flex-col gap-3">
39+
<Text variant="headingLg">{hazardModule.name}</Text>
40+
<Text variant="bodyMd" className="block min-h-[2lh]">
41+
{hazardModule.description || ''}
42+
</Text>
43+
{(districtCount != null || veryHighRiskCount != null) && (
44+
<div className="flex flex-wrap gap-2">
45+
{districtCount != null && districtCount > 0 && (
46+
<Tag variation="filled">
47+
{t('insights', { count: districtCount })}
48+
</Tag>
49+
)}
50+
{veryHighRiskCount != null &&
51+
veryHighRiskCount > 0 &&
52+
!isLoading && (
53+
<Tag variation="filled" fillColor="#FFF4F4" textColor="red">
54+
{t('veryHighRiskCount', { count: veryHighRiskCount })}
55+
</Tag>
56+
)}
57+
{isLoading && (
58+
<Tag variation="filled" fillColor="#FFF4F4" textColor="red">
59+
{tCommon('loading')}
60+
</Tag>
61+
)}
62+
</div>
63+
)}
64+
<Link
65+
href={routes.analytics(stateSlug, hazardModule.slug)}
66+
className="text-textLink flex items-center justify-end gap-2 hover:text-actionPrimaryBasicPressed"
67+
>
68+
<p>{t('explore')}</p>
69+
<IconArrowRight className="h-4 w-4 shrink-0" />
70+
</Link>
71+
</div>
72+
</div>
73+
);
74+
}
75+
76+
export default function StatePage() {
77+
const params = useParams();
78+
const t = useTranslations('analytics.hub');
79+
const stateName = useStateName();
80+
const state = states.find((s) => s.slug === params.state);
81+
82+
if (!state) {
83+
return <NotFound />;
84+
}
85+
86+
return (
87+
<div className="p-10 px-4 md:px-20 ">
88+
<div className="flex flex-col gap-4">
89+
<Text variant="heading3xl" fontWeight="bold">
90+
{t('heading', { stateName: stateName(state.slug, state.name) })}
91+
</Text>
92+
<Text variant="bodyLg">{t('description')}</Text>
93+
<Text variant="bodyLg" fontWeight="semibold">
94+
{t('emptyPrompt')}
95+
</Text>
96+
</div>
97+
<div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2 ">
98+
{(state.modules ?? []).map((hazardModule: Module) => {
99+
if (hazardModule.status === 'active') {
100+
return (
101+
<ModuleHubCard
102+
key={hazardModule.slug}
103+
stateSlug={state.slug}
104+
hazardModule={hazardModule}
105+
/>
106+
);
107+
}
108+
})}
109+
</div>
110+
<div
111+
className="mt-6 box-border flex w-full items-start gap-3 rounded-1 border-1 border-baseGreenSolid6 bg-baseGreenSolid4 p-5"
112+
role="note"
113+
>
114+
<IconInfoCircle className="h-6 w-6 shrink-0 text-iconDefault" />
115+
<div className="flex min-w-0 flex-1 flex-col items-start gap-2">
116+
<Text variant="bodyMd" fontWeight="bold" className="text-textMedium">
117+
{t('notes.divisionAnalytics.label')}
118+
<Text
119+
variant="bodyMd"
120+
fontWeight="regular"
121+
className="pl-1 text-textMedium"
122+
>
123+
{t('notes.divisionAnalytics.description')}
124+
</Text>
125+
</Text>
126+
127+
<Text variant="bodyMd" fontWeight="bold" className="text-textMedium">
128+
{t('notes.heat.label')}
129+
<Text
130+
variant="bodyMd"
131+
fontWeight="regular"
132+
className="pl-1 text-textMedium"
133+
>
134+
{t('notes.heat.description')}
135+
</Text>
136+
</Text>
137+
</div>
138+
</div>
139+
</div>
140+
);
141+
}

0 commit comments

Comments
 (0)