diff --git a/frontend/compositions/dashboard-header/dashboard-header.module.css b/frontend/compositions/dashboard-header/dashboard-header.module.css index 7748b9fff..896ef3862 100644 --- a/frontend/compositions/dashboard-header/dashboard-header.module.css +++ b/frontend/compositions/dashboard-header/dashboard-header.module.css @@ -5,7 +5,7 @@ .backgroundBanner { display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr auto 0.3fr; min-width: 100%; position: absolute; background-color: var(--lightBlue); diff --git a/frontend/compositions/dashboard-header/dashboard-header.tsx b/frontend/compositions/dashboard-header/dashboard-header.tsx index 79bba2e74..9c0ad4ace 100644 --- a/frontend/compositions/dashboard-header/dashboard-header.tsx +++ b/frontend/compositions/dashboard-header/dashboard-header.tsx @@ -3,7 +3,7 @@ import useDropdownMenu from "react-accessible-dropdown-menu-hook" import { useMediaQuery } from "react-responsive" import { AuthContext } from "../../helpers/auth" import { LogoSizes } from "../../models" -import { Logo as NPDCLogo } from "../../shared-components" +import { Logo as NPDCLogo, PrimaryButton } from "../../shared-components" import styles from "./dashboard-header.module.css" import MobileDropdown from "./mobile-dropdown" import Nav from "./nav" @@ -23,17 +23,6 @@ export default function DashboardHeader() {

National Police Data Coalition

The national index of police incidents

-
{/* Only show the buttons if the user is logged in */} @@ -46,6 +35,7 @@ export default function DashboardHeader() { )} )} + Donate ) diff --git a/frontend/compositions/index.tsx b/frontend/compositions/index.tsx index 5919e9afb..369fada4e 100644 --- a/frontend/compositions/index.tsx +++ b/frontend/compositions/index.tsx @@ -15,7 +15,7 @@ import SavedSearches from "./profile-saved-tables/saved-searches" import ProfileType from "./profile-type" import { SearchPanel } from "./search-panel/search-panel" import SearchResultsTable from "./search-results" -import { Map } from "./visualizations" +import { Map, BubbleChart } from "./visualizations" export { DashboardHeader, @@ -24,6 +24,7 @@ export { EnrollmentHeader, LandingPage, Map, + BubbleChart, PassportApplicationResponse, PasswordAid, ProfileInfo, diff --git a/frontend/compositions/search-panel/search-panel.tsx b/frontend/compositions/search-panel/search-panel.tsx index d9d0e1829..2bbc9033b 100644 --- a/frontend/compositions/search-panel/search-panel.tsx +++ b/frontend/compositions/search-panel/search-panel.tsx @@ -2,7 +2,7 @@ import { useState } from "react" import { FormProvider, useForm } from "react-hook-form" import { useAuth, useSearch } from "../../helpers" -import { searchPanelInputs, SearchTypes, ToggleOptions } from "../../models" +import { IToggleOptions, searchPanelInputs, SearchTypes } from "../../models" import { FormLevelError, PrimaryButton, @@ -12,10 +12,17 @@ import { } from "../../shared-components" import styles from "./search.module.css" import SecondaryInputStories from "../../shared-components/secondary-input/secondary-input.stories" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { faEdit } from "@fortawesome/free-solid-svg-icons" const { searchPanelContainer, searchForm } = styles -export const SearchPanel = () => { +interface SearchPanelProps { + toggleOptions: IToggleOptions[] + setToggleOptions: Function +} + +export const SearchPanel = (props: SearchPanelProps) => { const form = useForm() const { searchIncidents } = useSearch() const { accessToken } = useAuth() @@ -23,9 +30,7 @@ export const SearchPanel = () => { const [errorMessage, setErrorMessage] = useState("") const [formInputs, setFormInputs] = useState(searchPanelInputs.incidents) const [isLoading, setIsLoading] = useState(false) - const [toggleOptions, setToggleOptions] = useState( - new ToggleOptions("incidents", "officers").options - ) + const { toggleOptions, setToggleOptions } = props const toggleFormInputs = (e: any) => { const updatedToggleOptions = toggleOptions.map(({ type, value }) => { @@ -65,6 +70,7 @@ export const SearchPanel = () => { {errorMessage && } + Search diff --git a/frontend/compositions/search-panel/search.module.css b/frontend/compositions/search-panel/search.module.css index 7a3ab5043..5baf2b9f8 100644 --- a/frontend/compositions/search-panel/search.module.css +++ b/frontend/compositions/search-panel/search.module.css @@ -8,7 +8,7 @@ display: flex; flex-direction: column; align-items: center; - justify-content: space-evenly; + /* justify-content: space-evenly; */ width: var(--size384); height: 60vh; } diff --git a/frontend/compositions/search-results/search-results.tsx b/frontend/compositions/search-results/search-results.tsx index 9e9d2f696..e64c523d4 100644 --- a/frontend/compositions/search-results/search-results.tsx +++ b/frontend/compositions/search-results/search-results.tsx @@ -3,7 +3,7 @@ import { faSlidersH } from "@fortawesome/free-solid-svg-icons" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { useSearch } from "../../helpers" -import { Perpetrator } from "../../helpers/api" +import { Incident, Officer, Perpetrator, Rank } from "../../helpers/api" import { formatDate } from "../../helpers/syntax-helper" import { TooltipIcons, TooltipTypes } from "../../models" import { InfoTooltip } from "../../shared-components" @@ -13,16 +13,16 @@ import { GreaterThanButton } from "../../shared-components/icon-buttons/icon-buttons" -export default function SearchResultsTable() { - const { - incidentResults: { results } - } = useSearch() - - if (results.length === 0) return
No results
+interface SearchProps { + results: Incident[] | Officer[] + resultsColumns: Column[] +} +export default function SearchResultsTable(searchProps: SearchProps) { + const { results, resultsColumns } = searchProps return ( <> - {!!results.length ? ( + {!!searchProps.results.length ? ( ) : (

No Results

diff --git a/frontend/compositions/visualizations/bubble-chart/BubbleChart.stories.tsx b/frontend/compositions/visualizations/bubble-chart/BubbleChart.stories.tsx new file mode 100644 index 000000000..914ad5083 --- /dev/null +++ b/frontend/compositions/visualizations/bubble-chart/BubbleChart.stories.tsx @@ -0,0 +1,12 @@ +import React from "react" +import { ComponentMeta, ComponentStory } from "@storybook/react" +import BubbleChart from "./BubbleChart" + +export default { + title: "Visualizations/BubbleChart", + component: BubbleChart +} as ComponentMeta + +const Template: ComponentStory = () => + +export const Default = Template.bind({}) diff --git a/frontend/compositions/visualizations/bubble-chart/BubbleChart.tsx b/frontend/compositions/visualizations/bubble-chart/BubbleChart.tsx new file mode 100644 index 000000000..caf02b436 --- /dev/null +++ b/frontend/compositions/visualizations/bubble-chart/BubbleChart.tsx @@ -0,0 +1,189 @@ +import * as d3 from "d3" +import { useEffect, useRef } from "react" +import styles from "./bubble-chart.module.css" + +interface BubbleData { + officerName: string + location: string + incidents: number +} + +interface BubbleNode extends BubbleData { + children?: BubbleNode[] +} + +interface BubbleChartProps { + height?: number +} + +export default function BubbleChart({ height }: BubbleChartProps) { + const svgRef = useRef(null) + + const updateSVG = () => { + const svg = d3.select(svgRef.current) + + const root = d3.hierarchy(bubbleTree) + const pack = d3.pack().size([300, 300]).padding(5) + + root.sum((d) => d.incidents || 0).sort((a, b) => b.data.incidents - a.data.incidents) + const packedHierarchy = pack(root) + + const colorScale = d3 + .scaleOrdinal() + .domain(packedHierarchy.leaves().map((d) => d.data.officerName)) + .range(["#BBDDF8", "#7CAED7", "#303463"]) + + svg.selectAll("*").remove() + + // Add drop shadow to SVG + svg + .append("defs") + .append("filter") + .attr("id", "drop-shadow") + .attr("width", "150%") + .attr("height", "150%") + .append("feDropShadow") + .attr("dx", 0) + .attr("dy", 2) + .attr("stdDeviation", 1) + .attr("flood-color", "rgba(0,0,0,0.5)") + + // Add bubbles to SVG + svg + .selectAll("circle") + .data(packedHierarchy.leaves()) + .enter() + .append("circle") + .attr("cx", (d) => d.x) + .attr("cy", (d) => d.y) + .attr("r", (d) => d.r) + .attr("fill", (d) => colorScale(d.data.officerName)) + .attr("opacity", 0.7) + .style("filter", "url(#drop-shadow)") + + // Add bubble text to SVG + svg + .selectAll("foreignObject") + .data(packedHierarchy.leaves()) + .enter() + .append("foreignObject") + .attr("width", (d) => d.r * 2) + .attr("height", (d) => d.r * 2) + .html(({ data, r }) => renderBubbleText(data, r, colorScale)) + .attr("x", (d) => d.x - d.r) + .attr("y", (d) => d.y - d.r) + .attr("font-size", "11px") + + // Adjust viewBox to fit the entire graphic + const minX = d3.min(packedHierarchy.leaves(), (d) => d.x - d.r) + const minY = d3.min(packedHierarchy.leaves(), (d) => d.y - d.r) + const width = d3.max(packedHierarchy.leaves(), (d) => d.x + d.r - minX) + const height = d3.max(packedHierarchy.leaves(), (d) => d.y + d.r - minY) + + svg.attr("viewBox", `${minX - 5} ${minY - 5} ${width + 10} ${height + 10}`) + } + + useEffect(() => { + updateSVG() + }, []) + + return +} + +const renderBubbleText = ( + data: BubbleNode, + r: number, + colorScale: d3.ScaleOrdinal +) => { + const dummy = document.createElement("div") + const circleColor = colorScale(data.officerName) + const fillColor = circleColor === "#303463" ? "white" : "black" + + const bubbleText = document.createElement("div") + bubbleText.style.display = "flex" + bubbleText.style.flexDirection = "column" + bubbleText.style.justifyContent = "center" + bubbleText.style.alignItems = "center" + bubbleText.style.height = "100%" + bubbleText.style.color = fillColor + + // Officer Name Element + const officerName = document.createElement("tspan") + officerName.textContent = data.officerName + officerName.style.fontWeight = "bold" + officerName.style.textAlign = "center" + + // Location Element + const location = document.createElement("tspan") + location.textContent = data.location + + // Incident Element + const incidents = document.createElement("tspan") + incidents.textContent = `${data.incidents} Incidents` + + bubbleText.append(officerName) + bubbleText.append(location) + bubbleText.append(incidents) + + dummy.append(bubbleText) + + return r > 40 ? dummy.innerHTML : "" +} + +const bubbleTree: BubbleNode = { + officerName: "Root", + location: "", + incidents: 0, + children: [ + { + officerName: "Sgt. Jason Smith", + location: "New York PD", + incidents: 846 + }, + { + officerName: "Lt. Jason Smith", + location: "Seattle PD", + incidents: 92 + }, + { + officerName: "Sgt. Jason Smith", + location: "Hoover Sherriff", + incidents: 8 + }, + { + officerName: "Cpt. Jason Smith", + location: "Tampa PD", + incidents: 742 + }, + { + officerName: "Dep. Jason Smith", + location: "Honolulu PD", + incidents: 12 + }, + { + officerName: "Cpt. Sarah Johnson", + location: "Chicago PD", + incidents: 950 + }, + { + officerName: "Sgt. Emily Davis", + location: "Los Angeles PD", + incidents: 347 + }, + { + officerName: "Lt. Michael Brown", + location: "Dallas PD", + incidents: 580 + }, + { + officerName: "Dep. Amanda White", + location: "Miami PD", + incidents: 49 + }, + { + officerName: "Cpt. Robert Miller", + location: "Phoenix PD", + incidents: 15 + } + ] +} diff --git a/frontend/compositions/visualizations/bubble-chart/bubble-chart.module.css b/frontend/compositions/visualizations/bubble-chart/bubble-chart.module.css new file mode 100644 index 000000000..ecd7924e8 --- /dev/null +++ b/frontend/compositions/visualizations/bubble-chart/bubble-chart.module.css @@ -0,0 +1,3 @@ +.svg { + width: 100%; +} diff --git a/frontend/compositions/visualizations/bubble-chart/index.tsx b/frontend/compositions/visualizations/bubble-chart/index.tsx new file mode 100644 index 000000000..25db14a88 --- /dev/null +++ b/frontend/compositions/visualizations/bubble-chart/index.tsx @@ -0,0 +1,5 @@ +import dynamic from "next/dynamic" + +// d3 does not support server-side rendering, so only render the Map component +// in the browser. +export default dynamic(() => import("./BubbleChart"), { ssr: false }) diff --git a/frontend/compositions/visualizations/index.tsx b/frontend/compositions/visualizations/index.tsx index 8c4079cef..e4e7683ec 100644 --- a/frontend/compositions/visualizations/index.tsx +++ b/frontend/compositions/visualizations/index.tsx @@ -1,4 +1,5 @@ import Map from "./map" +import BubbleChart from "./bubble-chart" import useSearchData from "./map/useSearchData" -export { Map, useSearchData } +export { Map, useSearchData, BubbleChart } diff --git a/frontend/helpers/api/api.ts b/frontend/helpers/api/api.ts index 7247aca09..981e2c0da 100644 --- a/frontend/helpers/api/api.ts +++ b/frontend/helpers/api/api.ts @@ -50,9 +50,28 @@ export interface Perpetrator { last_name?: string } +export enum Rank { + TECHNICIAN = "Technician", + OFFICER = "Officer", + DETECTIVE = "Detective", + CORPORAL = "Corporal", + SERGEANT = "Sergeant", + LIEUTENANT = "Lieutenant", + CAPTAIN = "Captain", + DEPUTY = "Deputy", + CHIEF = "Chief" +} + export interface Officer { + id?: number first_name?: string last_name?: string + race?: string + ethnicity?: string + gender?: string + rank?: Rank + star?: string + date_of_birth?: Date } export interface UseOfForce { diff --git a/frontend/models/officer.ts b/frontend/models/officer.ts deleted file mode 100644 index aa2bde14e..000000000 --- a/frontend/models/officer.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Incident, Perpetrator } from "../helpers/api" - -export interface AgencyType { - agencyName: string - agencyImage: string - agencyHqAddress: string - websiteUrl: string -} - -export const agencyColumns = [ - { - Header: "agency", - accessor: "agency" - }, - { - Header: "Address", - accessor: "agencyHqAddress" - }, - { - Header: "Website", - accessor: "websiteUrl" - }, - { - Header: "Agency Address", - accessor: "agencyHqAddress" - } -] - -export interface EmploymentType { - agency: AgencyType - currentlyEmployed: boolean - earliestEmployment: Date - latestEmployment: Date - badgeNumber?: string -} - -export interface OfficerRecordType { - recordId: number - firstName: string - lastName: string - dateOfBirth?: Date - gender?: string - race?: string - ethnicity?: string - knownEmployers?: AgencyType[] - workHistory?: EmploymentType[] - accusations?: Perpetrator[] - affiliations?: OfficerRecordType[] -} diff --git a/frontend/models/officer.tsx b/frontend/models/officer.tsx new file mode 100644 index 000000000..532426864 --- /dev/null +++ b/frontend/models/officer.tsx @@ -0,0 +1,104 @@ +import { Column } from "react-table" +import { Incident, Perpetrator } from "../helpers/api" +import { CirclePlusButton } from "../shared-components/icon-buttons/icon-buttons" +import { InfoTooltip } from "../shared-components" +import { TooltipIcons, TooltipTypes } from "./info-tooltip" + +export interface AgencyType { + agencyName: string + agencyImage: string + agencyHqAddress: string + websiteUrl: string +} + +export const agencyColumns = [ + { + Header: "agency", + accessor: "agency" + }, + { + Header: "Address", + accessor: "agencyHqAddress" + }, + { + Header: "Website", + accessor: "websiteUrl" + }, + { + Header: "Agency Address", + accessor: "agencyHqAddress" + } +] + +export interface EmploymentType { + agency: AgencyType + currentlyEmployed: boolean + earliestEmployment: Date + latestEmployment: Date + badgeNumber?: string +} + +export interface OfficerRecordType { + recordId: number + firstName: string + lastName: string + dateOfBirth?: Date + gender?: string + race?: string + ethnicity?: string + knownEmployers?: AgencyType[] + workHistory?: EmploymentType[] + accusations?: Perpetrator[] + affiliations?: OfficerRecordType[] +} + +export const officerResultsColumns: Column[] = [ + { + Header: "Name", + accessor: (row: any) => `${row["first_name"]}, ${row["last_name"].charAt(0)}`, + id: "name" + }, + { + Header: () => ( + + Allegations + + + ), + accessor: "allegations", + id: "allegations" + }, + { + Header: "Race", + accessor: "race", + id: "race" + }, + { + Header: "Gender", + accessor: "gender", + id: "gender" + }, + { + Header: () => ( + + Rank + + + ), + accessor: "rank", + id: "rank" + }, + { + Header: "Employers", + accessor: "employers", + id: "employers" + }, + { + Header: "Save", + accessor: "save", + Cell: () => { + return console.log("clicked")} /> + }, + id: "save" + } +] diff --git a/frontend/models/primary-input.tsx b/frontend/models/primary-input.tsx index 32da47469..a6ff9b06d 100644 --- a/frontend/models/primary-input.tsx +++ b/frontend/models/primary-input.tsx @@ -160,8 +160,14 @@ export const searchPanelInputs: { [key in SearchTypes]: PrimaryInputNames[] } = [SearchTypes.OFFICERS]: [ PrimaryInputNames.OFFICER_NAME, PrimaryInputNames.LOCATION, - PrimaryInputNames.BADGE_NUMBER, - PrimaryInputNames.DATE_START, - PrimaryInputNames.DATE_END + PrimaryInputNames.BADGE_NUMBER + // PrimaryInputNames.DATE_START, + // PrimaryInputNames.DATE_END ] } + +export const primaryInputContent: { [key in string]: string } = { + [PrimaryInputNames.OFFICER_NAME]: "Accepts full or partial names and titles", + [PrimaryInputNames.LOCATION]: "Place where the officer may have worked", + [PrimaryInputNames.BADGE_NUMBER]: "If known, provide any badge number used" +} diff --git a/frontend/pages/search/index.tsx b/frontend/pages/search/index.tsx index 943b79602..bf40cbe77 100644 --- a/frontend/pages/search/index.tsx +++ b/frontend/pages/search/index.tsx @@ -1,21 +1,142 @@ -import { DashboardHeader, Map, SearchPanel, SearchResultsTable } from "../../compositions" +import { Column } from "react-table" +import { + BubbleChart, + DashboardHeader, + Map, + SearchPanel, + SearchResultsTable +} from "../../compositions" +import { resultsColumns } from "../../compositions/search-results/search-results" import { requireAuth, useSearch } from "../../helpers" +import { Officer, Rank } from "../../helpers/api" import { Layout } from "../../shared-components" import styles from "./search.module.css" +import { CirclePlusButton } from "../../shared-components/icon-buttons/icon-buttons" +import ErrorAlertDialog from "../../shared-components/error-alert-dialog/error-alert-dialog" +import { useState } from "react" +import { officerResultsColumns } from "../../models/officer" +import { SearchResultsTypes, ToggleOptions } from "../../models" export default requireAuth(function Dashboard() { - const { searchPageContainer, searchPageDisplay } = styles + const { searchPageContainer } = styles const { incidentResults } = useSearch() + const [toggleOptions, setToggleOptions] = useState( + new ToggleOptions("incidents", "officers").options + ) + + const isIncidentView = toggleOptions[0].value + const isOfficerView = toggleOptions[1].value return (
- -
- - {!!incidentResults && } + +
+ {isIncidentView && } + {isIncidentView && !!incidentResults && ( + + )} + + {isOfficerView && } + {isOfficerView && !!officerSearchResult && ( + + )}
) }) + +const officerSearchResult: Officer[] = [ + { + id: 1, + first_name: "John", + last_name: "Doe", + race: "White", + ethnicity: "Non-Hispanic", + gender: "Male", + rank: Rank.CAPTAIN, + star: "123456", + date_of_birth: new Date("01/01/1980") + }, + { + id: 1, + first_name: "John", + last_name: "Doe", + race: "White", + ethnicity: "Non-Hispanic", + gender: "Male", + rank: Rank.CAPTAIN, + star: "123456", + date_of_birth: new Date("01/01/1980") + }, + { + id: 1, + first_name: "John", + last_name: "Doe", + race: "White", + ethnicity: "Non-Hispanic", + gender: "Male", + rank: Rank.CAPTAIN, + star: "123456", + date_of_birth: new Date("01/01/1980") + }, + { + id: 1, + first_name: "John", + last_name: "Doe", + race: "White", + ethnicity: "Non-Hispanic", + gender: "Male", + rank: Rank.CAPTAIN, + star: "123456", + date_of_birth: new Date("01/01/1980") + }, + { + id: 1, + first_name: "John", + last_name: "Doe", + race: "White", + ethnicity: "Non-Hispanic", + gender: "Male", + rank: Rank.CAPTAIN, + star: "123456", + date_of_birth: new Date("01/01/1980") + }, + { + id: 1, + first_name: "John", + last_name: "Doe", + race: "White", + ethnicity: "Non-Hispanic", + gender: "Male", + rank: Rank.CAPTAIN, + star: "123456", + date_of_birth: new Date("01/01/1980") + }, + { + id: 1, + first_name: "John", + last_name: "Doe", + race: "White", + ethnicity: "Non-Hispanic", + gender: "Male", + rank: Rank.CAPTAIN, + star: "123456", + date_of_birth: new Date("01/01/1980") + }, + { + id: 1, + first_name: "John", + last_name: "Doe", + race: "White", + ethnicity: "Non-Hispanic", + gender: "Male", + rank: Rank.CAPTAIN, + star: "123456", + date_of_birth: new Date("01/01/1980") + } +] diff --git a/frontend/pages/search/search.module.css b/frontend/pages/search/search.module.css index b4d0173b4..aa0f0af05 100644 --- a/frontend/pages/search/search.module.css +++ b/frontend/pages/search/search.module.css @@ -1,12 +1,7 @@ .searchPageContainer { - display: flex; -} - -.searchPageDisplay { - display: flex; - flex-direction: column; -} - -.searchPageDisplay > * { - min-height: 50%; + max-height: 85vh; + overflow: auto; + display: grid; + grid-template-columns: 1fr 5fr; + grid-template-rows: repeat(2, 1fr); } diff --git a/frontend/shared-components/data-table/data-table-subcomps.tsx b/frontend/shared-components/data-table/data-table-subcomps.tsx index 99e5f1e4a..68172cad7 100644 --- a/frontend/shared-components/data-table/data-table-subcomps.tsx +++ b/frontend/shared-components/data-table/data-table-subcomps.tsx @@ -39,9 +39,9 @@ const PageButton = (props: PageButtonProps) => { const { icon, onclick, disabled } = props return ( - + ) } type PageNavigatorProps = { @@ -76,42 +76,14 @@ const PageNavigator = (props: PageNavigatorProps) => { return (
- Found {data.length.toLocaleString()} records - gotoPage(0)} - disabled={!canPreviousPage} - /> - previousPage()} disabled={!canPreviousPage} /> - - Page {pageIndex + 1} of {pageOptions.length}{" "} + {data.length} records found + + previousPage()} disabled={!canPreviousPage} /> + + {pageIndex + 1} of {pageOptions.length} + + nextPage()} disabled={!canNextPage} /> - nextPage()} disabled={!canNextPage} /> - gotoPage(pageCount - 1)} - disabled={!canNextPage} - /> - - Go to page:{" "} - { - const page = e.target.value ? Number(e.target.value) - 1 : 0 - gotoPage(page) - }} - style={{ width: "50px", textAlign: "right" }} - /> - {" "} -
) } diff --git a/frontend/shared-components/data-table/data-table.module.css b/frontend/shared-components/data-table/data-table.module.css index c36b26dd9..22b9415d8 100644 --- a/frontend/shared-components/data-table/data-table.module.css +++ b/frontend/shared-components/data-table/data-table.module.css @@ -12,7 +12,15 @@ text-align: center; } -.dataFooter, +.dataFooter { + background-color: var(--lightBlue); + display: flex; + justify-content: space-between; + padding-left: var(--size100); + padding-right: var(--size80); + padding-top: var(--size4); + padding-bottom: var(--size4); +} .dataHeader { background-color: var(--lightBlue); } @@ -123,3 +131,8 @@ -webkit-appearance: none; margin: 0; } + +.recordCount { + font-weight: bold; + font-size: var(--size18); +} diff --git a/frontend/shared-components/data-table/data-table.tsx b/frontend/shared-components/data-table/data-table.tsx index b24a3c1cf..190c78fc1 100644 --- a/frontend/shared-components/data-table/data-table.tsx +++ b/frontend/shared-components/data-table/data-table.tsx @@ -2,7 +2,7 @@ import { faArrowDown, faArrowUp } from "@fortawesome/free-solid-svg-icons" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import React, { useState } from "react" import { Column, defaultColumn, useFilters, usePagination, useSortBy, useTable } from "react-table" -import { Incident } from "../../helpers/api/api" +import { Incident, Officer } from "../../helpers/api/api" import { SavedResultsType, SavedSearchType } from "../../models" import { EditButton, PageNavigator } from "./data-table-subcomps" import styles from "./data-table.module.css" @@ -10,7 +10,7 @@ import styles from "./data-table.module.css" interface DataTableProps { tableName: string columns: Column[] - data: Incident[] | SavedSearchType[] | SavedResultsType[] | undefined + data: Incident[] | SavedSearchType[] | SavedResultsType[] | Officer[] | undefined } export function DataTable(props: DataTableProps) { @@ -60,11 +60,8 @@ export function DataTable(props: DataTableProps) { } return ( -
-
- {tableName} - -
+
+ {/*
*/} {headerGroups.map((headerGroup) => ( diff --git a/frontend/shared-components/error-alert-dialog/error-alert-dialog.module.css b/frontend/shared-components/error-alert-dialog/error-alert-dialog.module.css new file mode 100644 index 000000000..9581061d0 --- /dev/null +++ b/frontend/shared-components/error-alert-dialog/error-alert-dialog.module.css @@ -0,0 +1,24 @@ +.errorAlertDialogContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--size10) var(--size20) var(--size10) var(--size20); + border: 2px solid black; + border-radius: 5px; + /* height: 50%; */ +} + +.innerErrorAlertDialogContainer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding: var(--size20); + border: 3px solid red; + border-radius: 5px; +} + +.errorAlertDescription { + margin: 0 var(--size10) 0 var(--size10); +} diff --git a/frontend/shared-components/error-alert-dialog/error-alert-dialog.tsx b/frontend/shared-components/error-alert-dialog/error-alert-dialog.tsx new file mode 100644 index 000000000..e2939bcda --- /dev/null +++ b/frontend/shared-components/error-alert-dialog/error-alert-dialog.tsx @@ -0,0 +1,30 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import PrimaryButton from "../primary-button/primary-button" +import styles from "./error-alert-dialog.module.css" +import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons" +import { SearchResultsTypes, alertContent } from "../../models" + +interface ErrorAlertDialogProps { + setError: Function + searchResultType: SearchResultsTypes +} + +export default function ErrorAlertDialog(props: ErrorAlertDialogProps) { + const { errorAlertDialogContainer, innerErrorAlertDialogContainer, errorAlertDescription } = + styles + const { searchResultType } = props + return ( +
+
+ +
+

{alertContent[searchResultType]}

+

Please revise search or explore map

+
+
+ props.setError(false)}> + Return + +
+ ) +} diff --git a/frontend/shared-components/primary-button/primary-button.module.css b/frontend/shared-components/primary-button/primary-button.module.css index 4321e4f49..dd5cbcd7c 100644 --- a/frontend/shared-components/primary-button/primary-button.module.css +++ b/frontend/shared-components/primary-button/primary-button.module.css @@ -12,6 +12,7 @@ font-size: var(--size16); font-weight: 500; cursor: pointer; + padding: var(--size14); } .primaryButton:focus, .primaryButton:hover { diff --git a/frontend/shared-components/primary-input/primary-input.module.css b/frontend/shared-components/primary-input/primary-input.module.css index e944887a6..849d8a99b 100644 --- a/frontend/shared-components/primary-input/primary-input.module.css +++ b/frontend/shared-components/primary-input/primary-input.module.css @@ -25,3 +25,9 @@ .inputField[type="number"] { -moz-appearance: textfield; } + +.primarInputContent { + font-size: var(--size12); + margin-top: var(--size4); + margin-bottom: var(--size4); +} diff --git a/frontend/shared-components/primary-input/primary-input.tsx b/frontend/shared-components/primary-input/primary-input.tsx index 6bb991462..0f1355cd6 100644 --- a/frontend/shared-components/primary-input/primary-input.tsx +++ b/frontend/shared-components/primary-input/primary-input.tsx @@ -3,9 +3,17 @@ import React from "react" import { useFormContext } from "react-hook-form" import { FormLevelError } from ".." import { getTitleCaseFromCamel } from "../../helpers" -import { PrimaryInputNames, primaryInputValidation, TooltipTypes } from "../../models" +import { + primaryInputContent, + PrimaryInputNames, + primaryInputValidation, + SearchTypes, + TooltipTypes +} from "../../models" import InfoTooltip, { InfoTooltipProps } from "../info-tooltip/info-tooltip" import styles from "./primary-input.module.css" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { faInfoCircle } from "@fortawesome/free-solid-svg-icons" interface PrimaryInputProps { inputName: PrimaryInputNames @@ -30,7 +38,7 @@ export default function PrimaryInput({ register, formState: { errors } } = useFormContext() - const { inputContainer, inputField } = styles + const { inputContainer, inputField, primarInputContent } = styles const { errorMessage: defaultErrorMessage, pattern, @@ -71,6 +79,14 @@ export default function PrimaryInput({ defaultValue={defaultValue} {...register(inputName, { required: isRequired, pattern })} /> + {primaryInputContent[inputName] ? ( +

+ {" "} + {primaryInputContent[inputName]} +

+ ) : ( + <> + )} {!isValid && ( The national index of police incidents

- +