Skip to content

Commit 4096d66

Browse files
authored
Complete the search flow and add other details pages (#539)
* Adds links to search results. - Adds href links to search results based on content type and uid. - Disable static export in next.config.ts to allow for dynamic routing. - Renames officer page to use dynamic routing with uid. * Fix formatting * Convert IdentityCard component into subcomponents IdentityCard is used for both officers and agencies, but the information displayed is different for each. This commit breaks IdentityCard into three components: a base IdentityCard component that contains shared styles and structure, and two subcomponents OfficerIdentityCard and AgencyIdentityCard that contain the specific information for each type. Only the officer card is updated in this commit. * Convert the other details components ContentDetails and DetailsTabs are being split into separate components for officers and agencies. This commit updates the officer details page to use the new components, and also updates the content details component to handle cases where sources may not have names. * Fix formatting * Update cloud deployment configuration * Fix formatting
1 parent 6f51f9f commit 4096d66

17 files changed

Lines changed: 566 additions & 352 deletions

frontend/Dockerfile.cloud

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
# From repository root folder
33
# docker build --build-arg ENV=development -t police-data-trust-frontend-dev -f frontend/Dockerfile.cloud ./frontend
44

5-
FROM node:20-bookworm-slim as builder
5+
FROM node:20-bookworm-slim AS builder
66

7-
WORKDIR /app/
7+
WORKDIR /app
88
ARG ENV
99

1010
COPY package*.json ./
@@ -14,7 +14,17 @@ COPY . .
1414

1515
RUN npm run build:${ENV}
1616

17-
FROM nginx:1.27-bookworm
18-
COPY --from=builder /app/out /usr/share/nginx/html
19-
COPY --from=builder /app/nginx/default.conf /etc/nginx/conf.d/default.conf
20-
EXPOSE 80
17+
FROM node:20-bookworm-slim AS runner
18+
19+
WORKDIR /app
20+
ENV NODE_ENV=production
21+
ENV PORT=3000
22+
ENV HOSTNAME=0.0.0.0
23+
24+
COPY --from=builder /app/.next/standalone ./
25+
COPY --from=builder /app/.next/static ./.next/static
26+
COPY --from=builder /app/public ./public
27+
28+
EXPOSE 3000
29+
30+
CMD ["node", "server.js"]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"use client"
2+
3+
import { useEffect, useState } from "react"
4+
import { useAuth } from "@/providers/AuthProvider"
5+
import { apiFetch } from "@/utils/apiFetch"
6+
import API_ROUTES, { apiBaseUrl } from "@/utils/apiRoutes"
7+
import { useParams } from "next/navigation"
8+
import { Officer } from "@/utils/api"
9+
import DetailsLayout from "@/components/Details/DetailsLayout"
10+
import OfficerIdentityCard from "@/components/Details/IdentityCard/OfficerIdentityCard"
11+
import OfficerDetailsTabs from "@/components/Details/tabs/OfficerDetailsTabs"
12+
import OfficerContentDetails from "@/components/Details/ContentDetails/OfficerContentDetails"
13+
14+
export default function OfficerDetailsPage() {
15+
const params = useParams<{ uid: string }>()
16+
const uid = params.uid
17+
18+
const [officer, setOfficer] = useState<Officer | null>(null)
19+
const { accessToken } = useAuth()
20+
const [loading, setLoading] = useState(true)
21+
22+
useEffect(() => {
23+
if (!accessToken || !uid) return
24+
25+
setLoading(true)
26+
27+
apiFetch(
28+
`${apiBaseUrl}${API_ROUTES.officers.profile(uid)}?include=employment&include=allegations`,
29+
{
30+
headers: {
31+
Authorization: `Bearer ${accessToken}`
32+
}
33+
}
34+
)
35+
.then((res) => res.json())
36+
.then((data) => setOfficer(data.results || data))
37+
.finally(() => setLoading(false))
38+
}, [accessToken, uid])
39+
40+
if (loading) return <div>Loading...</div>
41+
if (!officer) return <div>Officer not found</div>
42+
43+
return (
44+
<DetailsLayout sidebar={<OfficerContentDetails officer={officer} />}>
45+
<OfficerIdentityCard {...officer} />
46+
<OfficerDetailsTabs {...officer} />
47+
</DetailsLayout>
48+
)
49+
}

frontend/app/officer/page.tsx

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

frontend/app/profile/edit/EditProfilePage.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
.bioCount {
3131
font-size: 0.875rem;
32-
color: #666;
32+
color: #566;
3333
margin: 0;
3434
}
3535

Lines changed: 88 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use client"
2-
import { Tab, Tabs, Box, CardHeader, Typography } from "@mui/material"
2+
3+
import Link from "next/link"
4+
import { Tab, Tabs, Box, Card, CardHeader, CardActionArea, Typography } from "@mui/material"
35
import React from "react"
46
import { SearchResponse } from "@/utils/api"
57

@@ -10,6 +12,23 @@ type SearchResultsProps = {
1012
updateTab: (val: number) => void
1113
}
1214

15+
const getResultHref = (result: SearchResponse) => {
16+
switch (result.content_type) {
17+
case "Officer":
18+
return `/officer/${result.uid}`
19+
case "Agency":
20+
return `/agency/${result.uid}`
21+
case "Unit":
22+
return `/unit/${result.uid}`
23+
case "Complaint":
24+
return `/complaint/${result.uid}`
25+
case "Litigation":
26+
return `/litigation/${result.uid}`
27+
default:
28+
return "#"
29+
}
30+
}
31+
1332
const SearchResults = ({ total, results, tab, updateTab }: SearchResultsProps) => {
1433
return (
1534
<>
@@ -31,51 +50,78 @@ const SearchResults = ({ total, results, tab, updateTab }: SearchResultsProps) =
3150
<Tab key="litigation" label="Litigation" />
3251
</Tabs>
3352
</Box>
53+
3454
<Box sx={{ p: 3 }}>
35-
<Typography sx={{ marginBottom: "1rem", fontWeight: "bold" }}>{total} results</Typography>
55+
<Typography sx={{ mb: 2, fontWeight: "bold" }}>{total} results</Typography>
56+
3657
<CustomTabPanel value={tab} index={tab}>
37-
{results.map((result) => (
38-
<CardHeader
58+
{results.map((result, idx) => (
59+
<Card
3960
key={result.uid}
40-
title={result.title}
41-
subheader={result.subtitle}
42-
slotProps={{ subheader: { fontWeight: "bold", color: "#000" } }}
43-
action={
44-
<Box>
45-
<Box sx={{ display: "flex", gap: "1rem" }}>
46-
<span style={{ fontSize: "14px", color: "#454C54", margin: "0 0 1rem 0" }}>
47-
{result.details}
48-
</span>
49-
</Box>
50-
<Box sx={{ display: "flex", gap: "1rem" }}>
51-
<span style={{ fontSize: "12px", color: "#666" }}>{result.content_type}</span>
52-
<span style={{ fontSize: "12px", color: "#666" }}>{result.source}</span>
53-
<span style={{ fontSize: "12px", color: "#666" }}>{result.last_updated}</span>
54-
</Box>
55-
</Box>
56-
}
61+
variant="outlined"
5762
sx={{
58-
flexDirection: "column",
59-
alignItems: "flex-start",
60-
gap: "0.5rem",
61-
border: "1px solid #ddd",
62-
borderBottom: "none",
63-
":first-of-type": {
64-
borderTopLeftRadius: "4px",
65-
borderTopRightRadius: "4px"
66-
},
67-
":last-of-type": {
68-
borderBottomLeftRadius: "4px",
69-
borderBottomRightRadius: "4px",
70-
borderBottom: "1px solid #ddd"
71-
},
72-
"& .MuiCardHeader-content": {
73-
overflow: "hidden"
74-
},
75-
paddingInline: "4.5rem",
76-
paddingBlock: "2rem"
63+
borderBottomLeftRadius: idx === results.length - 1 ? "4px" : 0,
64+
borderBottomRightRadius: idx === results.length - 1 ? "4px" : 0,
65+
borderTopLeftRadius: idx === 0 ? "4px" : 0,
66+
borderTopRightRadius: idx === 0 ? "4px" : 0,
67+
borderBottom: idx === results.length - 1 ? undefined : "none"
7768
}}
78-
/>
69+
>
70+
<CardActionArea
71+
component={Link}
72+
href={getResultHref(result)}
73+
sx={{
74+
display: "block",
75+
textAlign: "left",
76+
"&:hover": {
77+
backgroundColor: "#f8f8f8"
78+
}
79+
}}
80+
>
81+
<CardHeader
82+
title={result.title}
83+
subheader={result.subtitle}
84+
slotProps={{ subheader: { fontWeight: "bold", color: "#000" } }}
85+
action={
86+
<Box>
87+
<Box sx={{ display: "flex", gap: "1rem" }}>
88+
<span
89+
style={{
90+
fontSize: "14px",
91+
color: "#454C54",
92+
margin: "0 0 1rem 0"
93+
}}
94+
>
95+
{Array.isArray(result.details)
96+
? result.details.join(", ")
97+
: result.details}
98+
</span>
99+
</Box>
100+
101+
<Box sx={{ display: "flex", gap: "1rem" }}>
102+
<span style={{ fontSize: "12px", color: "#566" }}>
103+
{result.content_type}``
104+
</span>
105+
<span style={{ fontSize: "12px", color: "#566" }}>{result.source}</span>
106+
<span style={{ fontSize: "12px", color: "#566" }}>
107+
{result.last_updated}
108+
</span>
109+
</Box>
110+
</Box>
111+
}
112+
sx={{
113+
flexDirection: "column",
114+
alignItems: "flex-start",
115+
gap: "0.5rem",
116+
"& .MuiCardHeader-content": {
117+
overflow: "hidden"
118+
},
119+
paddingInline: "4.5rem",
120+
paddingBlock: "2rem"
121+
}}
122+
/>
123+
</CardActionArea>
124+
</Card>
79125
))}
80126
</CustomTabPanel>
81127
</Box>
@@ -102,4 +148,5 @@ const CustomTabPanel = ({ children, value, index, ...other }: TabPanelProps) =>
102148
</div>
103149
)
104150
}
151+
105152
export default SearchResults

0 commit comments

Comments
 (0)