Skip to content

Commit

Permalink
Merge pull request #765 from 18F/bjs-start-ori-search
Browse files Browse the repository at this point in the history
First pass at agency search functionality
  • Loading branch information
jeremiak authored May 23, 2017
2 parents e21faa7 + 3bf0063 commit 2cf3854
Show file tree
Hide file tree
Showing 36 changed files with 416 additions and 132 deletions.
File renamed without changes.
1 change: 1 addition & 0 deletions public/data/agencies-by-state.json

Large diffs are not rendered by default.

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions public/img/x-navy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 8 additions & 4 deletions sass/components/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
// subset of wtf-forms
// http://wtfforms.com/

.control {
padding-left: 1.5rem;
}

.checkbox,
.radio {
cursor: pointer;
Expand Down Expand Up @@ -72,7 +76,7 @@

.checkbox input:checked ~ .indicator,
.radio input:checked ~ .indicator, {
background-color: $blue-gray;
background-color: $blue;
color: $white;
}

Expand All @@ -84,8 +88,8 @@

.checkbox .indicator {
background-color: $white;
border: $border-width solid $border-color;
border-radius: .25rem;
border: $border-width solid $blue;
border-radius: 2px;
}

.checkbox input:checked ~ .indicator {
Expand All @@ -94,7 +98,7 @@

.radio .indicator {
background-color: #f2f9ff;
border: $border-width solid $blue-gray;
border: $border-width solid $blue;
border-radius: 50%;
}

Expand Down
1 change: 1 addition & 0 deletions sass/util/_border.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
.border-w2 { border-width: 2px; }
.border-w8 { border-width: 8px; }

.rounded-none { border-radius: 0; }
.rounded-br { border-bottom-right-radius: $border-radius; }

@media #{$breakpoint-md} {
Expand Down
1 change: 1 addition & 0 deletions scripts/orisByUsState/agencies.json

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions scripts/orisByUsState/munge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const fs = require('fs')
const path = require('path')

const jsonFile = require('./agencies.json')

const agencies = jsonFile.results
const usStates = {}

agencies.forEach(agency => {
const stateAbbr = agency.state_abbr

const smallAgency = {
agency_name: agency.agency_name,
}

if (usStates[stateAbbr]) {
usStates[stateAbbr][agency.ori] = smallAgency
} else {
usStates[stateAbbr] = { [agency.ori]: smallAgency }
}
})

const onWriteDone = err => {
if (err) throw err

console.log('done!')

Object.keys(usStates).sort((a, b) => a - b).forEach(state => {
const agenciesCount = Object.keys(usStates[state]).length
console.log(`${state} has ${agenciesCount} agencies`)
})
}

const file = path.join(__dirname, '../../data/agencies-by-state.json')

fs.writeFile(file, JSON.stringify(usStates), onWriteDone)
4 changes: 2 additions & 2 deletions src/actions/agency.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const fetchAgency = params => dispatch => {
dispatch(fetchingAgency())

return api.getAgency(params.place).then(data => {
dispatch(receivedAgency(data))
const agency = data[params.place]
dispatch(receivedAgency(agency))
})
}

3 changes: 2 additions & 1 deletion src/components/AgencyChartContainer.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import startCase from 'lodash.startcase'
import uniqBy from 'lodash.uniqby'
import React from 'react'
import { connect } from 'react-redux'
Expand All @@ -23,7 +24,7 @@ const AgencyChartContainer = ({ crime, place, summary }) => {
<div>
<div className="mb2 p2 sm-p4 bg-white border-top border-blue border-w8">
<h2 className="mt0 mb3 fs-24 sm-fs-32 sans-serif">
{crime} reported by {place}
{startCase(crime)} reported by {place}
</h2>
{content}
</div>
Expand Down
85 changes: 85 additions & 0 deletions src/components/AgencySearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { Component } from 'react'

import AgencySearchResults from './AgencySearchResults'

class AgencySearch extends Component {
state = { search: '', displaySelection: true }

handleChange = e => {
this.setState({ search: e.target.value })
}

handleClick = datum => e => {
e.preventDefault()
this.setState({ displaySelection: true })
this.props.onChange({
place: datum.ori,
placeType: 'agency',
})
}

removeSelection = () => {
this.setState({ displaySelection: false, search: '' })
}

render() {
const { agencies, ori, state } = this.props
const { displaySelection, search } = this.state

const data = Object.keys(agencies[state]).map(agencyOri => {
const agency = agencies[state][agencyOri]
return { ori: agencyOri, ...agency }
})
const selected = data.find(d => d.ori === ori)

const searchUpper = search.toUpperCase()
const dataFiltered = searchUpper === ''
? data
: data.filter(d => {
const words = `${d.ori} ${d.agency_name}`.toUpperCase()
return words.includes(searchUpper)
})

const showOris = !!search.length && dataFiltered.length > 0

return (
<div className="mt2">
{selected && displaySelection
? <div className="mb2 relative">
<input
type="text"
className="col-12 field field-sm bg-white border-blue pr5 truncate"
defaultValue={selected.agency_name}
/>
<button
className="absolute btn p0 line-height-1"
style={{ top: '.5rem', right: '1rem' }}
onClick={this.removeSelection}
>
<img src="/img/x-navy.svg" alt="close" width="12" height="12" />
</button>
</div>
: <div className="relative">
<div className="relative">
<input
type="text"
className="col-12 field field-sm bg-white border-blue rounded-none"
placeholder="Search for an agency..."
value={search}
onChange={this.handleChange}
/>
</div>
{showOris &&
<AgencySearchResults
data={dataFiltered.sort(
(a, b) => a.agency_name > b.agency_name,
)}
onClick={this.handleClick}
/>}
</div>}
</div>
)
}
}

export default AgencySearch
46 changes: 46 additions & 0 deletions src/components/AgencySearchRefine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react'

const agencyTypes = [
'City',
'County',
'Federal',
'State Police',
'University or college',
'Tribal',
'Other',
]

const AgencySearchRefine = ({ keyword, onChange, onSubmit }) => (
<div
className="p2 absolute col-12 border-box bg-white border"
style={{ marginTop: -1, minHeight: 280 }}
>
<div>
<label className="mb-tiny fs-18 bold block">Keyword</label>
<input
className="mb2 col-12 field field-sm bg-white border-blue"
type="text"
name="keyword"
value={keyword}
onChange={onChange}
/>
</div>
<div>
<label className="mb-tiny fs-18 bold block">Agency type</label>
{agencyTypes.map((d, i) => (
<label key={i} className="block control checkbox">
<input type="checkbox" value={d} />
<span className="indicator" />
<span className="fs-12">{d}</span>
</label>
))}
</div>
<div className="mt1">
<button className="btn btn-sm btn-primary bg-navy" onClick={onSubmit}>
View results
</button>
</div>
</div>
)

export default AgencySearchRefine
26 changes: 26 additions & 0 deletions src/components/AgencySearchResults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react'

const AgencySearchResults = ({ data, onClick }) => (
<div
className="mb2 p2 absolute col-12 border-box bg-white border overflow-auto"
style={{ marginTop: -1, maxHeight: 240 }}
>
<div className="mb1 fs-20 bold line-height-2">Results</div>
<ul className="m0 list-reset fs-12">
{data.slice(0, 100).map((d, i) => (
<li key={i} className="">
<a
className="block black truncate"
style={{ lineHeight: '1.75' }}
href="#!"
onClick={onClick(d)}
>
{d.agency_name}
</a>
</li>
))}
</ul>
</div>
)

export default AgencySearchResults
2 changes: 1 addition & 1 deletion src/components/DownloadBulkNibrs.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import startCase from 'lodash.startcase'
import React from 'react'

import Term from './Term'
import ucrProgram from '../../data/ucr-program-participation.json'
import ucrProgram from '../../public/data/ucr-program-participation.json'

/* these codes are repeated here (seemingly also in util/api.js) because
different codes are needed for different parts of the application
Expand Down
24 changes: 7 additions & 17 deletions src/components/Explorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,23 @@ import ExplorerHeader from './ExplorerHeader'
import NibrsContainer from './NibrsContainer'
import NotFound from './NotFound'
import SidebarContainer from './SidebarContainer'
import SparklineSection from './SparklineSection'
import SparklineContainer from './SparklineContainer'
import TrendContainer from './TrendContainer'
import UcrParticipationContainer from './UcrParticipationContainer'
import { updateApp } from '../actions/composite'
import { showTerm } from '../actions/glossary'
import { hideSidebar, showSidebar } from '../actions/sidebar'
import offenses from '../util/offenses'
import { getPlaceInfo } from '../util/place'
import ucrParticipation from '../util/ucr'
import lookup, { nationalKey } from '../util/usa'

const getPlaceInfo = ({ place, placeType }) => ({
place: place || nationalKey,
placeType: placeType || 'national',
})
import lookup from '../util/usa'

class Explorer extends React.Component {
componentDidMount() {
const { appState, dispatch, router } = this.props
const { since, until } = appState.filters
const { query } = router.location
const { place } = getPlaceInfo(appState.filters)
const { place, placeType } = getPlaceInfo(appState.filters)

const clean = (val, alt) => {
const yr = +val
Expand All @@ -35,6 +31,7 @@ class Explorer extends React.Component {

const filters = {
place,
placeType,
...this.props.filters,
...router.params,
since: clean(query.since, since),
Expand Down Expand Up @@ -95,21 +92,14 @@ class Explorer extends React.Component {
<div className="site-content">
<div className="container-main mx-auto px2 md-py3 lg-px8">
<ExplorerHeader
agency={agencies}
agencies={agencies}
crime={crime}
place={place}
placeType={placeType}
/>
<UcrParticipationContainer />
<hr className="mt0 mb3" />
{isAgency &&
<SparklineSection
crime={crime}
place={place}
since={filters.since}
summaries={summaries}
until={filters.until}
/>}
{isAgency && <SparklineContainer />}
{isAgency ? <AgencyChartContainer /> : <TrendContainer />}
{showNibrs && <NibrsContainer />}
<hr className="mt0 mb3" />
Expand Down
9 changes: 5 additions & 4 deletions src/components/ExplorerHeader.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import startCase from 'lodash.startcase'
import React from 'react'

const getTitle = ({ agency, crime, place, placeType }) => {
const info = agency[place]
import { getAgency } from '../util/ori'

const getTitle = ({ agencies, crime, place, placeType }) => {
if (placeType !== 'agency') return `${startCase(place)} | ${startCase(crime)}`
if (agency.loading || !info) return 'Agency loading...'
return `${info.agency_name} Police Department | ${startCase(crime)}`

const info = getAgency(agencies, place)
return `${info.agency_name} | ${startCase(crime)}`
}

const ExplorerHeader = params => (
Expand Down
2 changes: 1 addition & 1 deletion src/components/Glossary.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import React from 'react'

import { hideGlossary, showGlossary } from '../actions/glossary'
import terms from '../../data/terms.json'
import terms from '../../content/terms.json'

let GlossaryPanel
if (typeof window !== 'undefined') GlossaryPanel = require('glossary-panel')
Expand Down
4 changes: 2 additions & 2 deletions src/components/Home.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ const Home = ({ appState, dispatch, router }) => {
const id = e.target.getAttribute('id')
if (!id) return

const placeNew = { place: slugify(stateLookup(id)) }
const placeNew = { place: slugify(stateLookup(id)), placeType: 'state' }
dispatch(updateFilters(placeNew))
dispatch(updateApp({ crime, ...placeNew }, router))
}

const handleSearchClick = () => {
const change = { crime, place }
const change = { crime, place, placeType: 'state' }
dispatch(updateApp(change, router))
}

Expand Down
Loading

0 comments on commit 2cf3854

Please sign in to comment.