├── .nvmrc ├── .prettierignore ├── src ├── styles │ ├── components │ │ ├── search-results.scss │ │ ├── navbar.scss │ │ ├── LineGraph.scss │ │ ├── AreaChart.scss │ │ ├── search-result-group.scss │ │ ├── PermitsTable.scss │ │ ├── PieChart.scss │ │ ├── GranularVolume.scss │ │ ├── Workflow.scss │ │ ├── topic-tile.scss │ │ ├── treeMap.scss │ │ ├── DetailsTable.scss │ │ ├── search-result.scss │ │ ├── home.scss │ │ ├── search.scss │ │ ├── PermitTimeline.scss │ │ ├── leaflet.scss │ │ ├── components.scss │ │ ├── Permit.scss │ │ ├── TimeSlider.scss │ │ ├── filterCheckbox.scss │ │ ├── TopicCard.scss │ │ ├── search-bar.scss │ │ ├── collapsible.scss │ │ ├── projects.scss │ │ ├── disclaimer.scss │ │ ├── Accordion.scss │ │ ├── DataModal.scss │ │ └── multiSelect.scss │ └── bootstrap │ │ ├── mixins │ │ ├── _center-block.scss │ │ ├── _opacity.scss │ │ ├── _size.scss │ │ ├── _text-overflow.scss │ │ ├── _labels.scss │ │ ├── _resize.scss │ │ ├── _progress-bar.scss │ │ ├── _text-emphasis.scss │ │ ├── _reset-filter.scss │ │ ├── _nav-divider.scss │ │ ├── _background-variant.scss │ │ ├── _alerts.scss │ │ ├── _tab-focus.scss │ │ ├── _nav-vertical-align.scss │ │ ├── _reset-text.scss │ │ ├── _border-radius.scss │ │ ├── _pagination.scss │ │ ├── _responsive-visibility.scss │ │ ├── _panels.scss │ │ ├── _hide-text.scss │ │ ├── _clearfix.scss │ │ ├── _list-group.scss │ │ ├── _table-row.scss │ │ ├── _image.scss │ │ └── _buttons.scss │ │ ├── _wells.scss │ │ ├── _responsive-embed.scss │ │ ├── _breadcrumbs.scss │ │ ├── _close.scss │ │ ├── _component-animations.scss │ │ ├── _thumbnails.scss │ │ ├── _pager.scss │ │ ├── _mixins.scss │ │ ├── _utilities.scss │ │ ├── _media.scss │ │ ├── bootstrap.scss │ │ ├── _jumbotron.scss │ │ ├── _badges.scss │ │ ├── _labels.scss │ │ ├── _code.scss │ │ ├── _grid.scss │ │ ├── _alerts.scss │ │ └── _pagination.scss ├── images │ ├── Car.png │ ├── City.png │ ├── Cook.png │ ├── Fence.png │ ├── Fire.png │ ├── Gun.png │ ├── Home2.png │ ├── Mug.png │ ├── User.png │ ├── AidKit2.png │ ├── Bubble.png │ ├── Dollar.png │ ├── Hammer.png │ ├── Office.png │ ├── Pencil7.png │ ├── Profile.png │ ├── Shield3.png │ ├── Users4.png │ ├── Ambulance.png │ ├── BillDollar.png │ ├── Direction.png │ ├── Ellipsis.png │ ├── Library2.png │ ├── marker-icon.png │ ├── climate │ │ ├── cji-map.jpg │ │ ├── cji-storymap.jpg │ │ ├── resiliency-guide.jpg │ │ └── sustainability-webpage.jpg │ ├── marker-icon-2.png │ └── citylogo-flatblue.png ├── app │ ├── spatial_event_topic_summary │ │ ├── english.js │ │ ├── spanish.js │ │ ├── spatialEventTopicFilters.css │ │ └── SpatialEventTopicLocationInfo.js │ ├── utils.js │ ├── capital_projects │ │ ├── citylogo-419x314.png │ │ ├── CIPTextReplacements.js │ │ ├── CIPColors.js │ │ ├── CIPIcons.js │ │ └── CIPData.js │ ├── search │ │ ├── graphql │ │ │ ├── searchDefaultState.js │ │ │ ├── searchQueries.js │ │ │ ├── searchMutations.js │ │ │ └── searchResolvers.js │ │ ├── searchResults │ │ │ ├── powered_by_google_on_white.png │ │ │ └── searchResultGroup.css │ │ ├── styles.css │ │ ├── searchByEntities │ │ │ ├── searchByEntities.css │ │ │ ├── SearchByEntity.js │ │ │ └── SearchByEntities.js │ │ └── Search.js │ ├── MySimpliCity.js │ ├── development │ │ ├── sla_dashboard │ │ │ ├── SLADashboardQueries.js │ │ │ └── SLA_utilities.js │ │ ├── trc │ │ │ ├── SubNode.js │ │ │ ├── PermitTypeCards.js │ │ │ ├── LargeNodeWrapper.js │ │ │ ├── NodeSteps.js │ │ │ ├── PermitTypeCard.js │ │ │ ├── ArrowDefs.js │ │ │ └── TRCDataTable.js │ │ ├── volume │ │ │ ├── LoadingModal.js │ │ │ ├── DataModal.js │ │ │ ├── StatusDash.js │ │ │ ├── PermitDataQuery.js │ │ │ ├── ChildMenus.js │ │ │ ├── PermitVolCirclepack.js │ │ │ ├── HierarchicalDropdown.js │ │ │ ├── dotBinLayout.js │ │ │ ├── PermitTypeMenus.js │ │ │ └── GranularDash.js │ │ └── permits │ │ │ └── PermitsMap.js │ ├── Locations.js │ ├── internal │ │ └── bpt_projects │ │ │ └── ProjectFlowQueries.js │ ├── budget │ │ ├── graphql │ │ │ ├── budgetDefaultState.js │ │ │ ├── budgetQueries.js │ │ │ ├── budgetMutations.js │ │ │ └── budgetResolvers.js │ │ ├── BudgetData.js │ │ └── BudgetSankey.js │ ├── Banner.js │ ├── Home.js │ ├── crime │ │ ├── english.js │ │ └── spanish.js │ ├── Footer.js │ ├── CityInfoBar.js │ ├── Topics.js │ ├── EnvBanner.js │ ├── climate │ │ └── ClickableTile.js │ └── address │ │ ├── english.js │ │ └── spanish.js ├── shared │ ├── NotFound.js │ ├── NoResults.js │ ├── LinkButton.js │ ├── ButtonGroup.js │ ├── GetVersion.js │ ├── FilterRenderer.js │ ├── ErrorBoundary.js │ ├── Error.js │ ├── visualization │ │ ├── Tooltip.js │ │ ├── BigNumber.js │ │ ├── MapLegendControl.js │ │ ├── HorizontalLegend.js │ │ └── hashid.js │ ├── DropDownButton.js │ ├── Checkbox.js │ ├── LinkFocusWrapper.js │ ├── DetailsFormGroup.js │ ├── DetailsIconLinkFormGroup.js │ ├── Icon.js │ ├── Select.js │ ├── InCityMessage.js │ ├── EmailDownload.js │ ├── LoadingAnimation.js │ ├── Button.js │ ├── DetailsTable.js │ ├── DetailsIconLinkGrouping.js │ └── react_table_hoc │ │ └── ExpandingRows.js ├── defaultState.js ├── resolvers.js ├── utilities │ ├── auth │ │ ├── graphql │ │ │ ├── authDefaultState.js │ │ │ ├── authQueries.js │ │ │ ├── authMutations.js │ │ │ └── authResolvers.js │ │ └── authProviderModal.js │ ├── lang │ │ ├── LanguageContext.js │ │ └── LangSwitcher.js │ ├── generalUtilities.js │ ├── counterSet.js │ ├── statistics.js │ ├── timeSeriesSet.js │ └── dateUtilities.js ├── index.js ├── hooks │ ├── useDebounce.js │ └── useLocalStorage.js ├── fragmentTypes.js └── gqlClient.js ├── public ├── favicon.ico ├── manifest.json └── index.html ├── database.rules.json ├── example.env ├── .editorconfig ├── docs ├── testing │ └── readme.md ├── development-resources │ └── readme.md ├── notes │ └── readme.md ├── index.md ├── deployment │ └── readme.md └── requirements │ └── principles.md ├── .gitignore ├── README.md ├── .eslintrc-initial-version.json └── .gitattributes /.nvmrc: -------------------------------------------------------------------------------- 1 | v14 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/components/search-results.scss: -------------------------------------------------------------------------------- 1 | .search-results { 2 | margin-top: 60px; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/images/Car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Car.png -------------------------------------------------------------------------------- /src/images/City.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/City.png -------------------------------------------------------------------------------- /src/images/Cook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Cook.png -------------------------------------------------------------------------------- /src/images/Fence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Fence.png -------------------------------------------------------------------------------- /src/images/Fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Fire.png -------------------------------------------------------------------------------- /src/images/Gun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Gun.png -------------------------------------------------------------------------------- /src/images/Home2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Home2.png -------------------------------------------------------------------------------- /src/images/Mug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Mug.png -------------------------------------------------------------------------------- /src/images/User.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/User.png -------------------------------------------------------------------------------- /src/images/AidKit2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/AidKit2.png -------------------------------------------------------------------------------- /src/images/Bubble.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Bubble.png -------------------------------------------------------------------------------- /src/images/Dollar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Dollar.png -------------------------------------------------------------------------------- /src/images/Hammer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Hammer.png -------------------------------------------------------------------------------- /src/images/Office.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Office.png -------------------------------------------------------------------------------- /src/images/Pencil7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Pencil7.png -------------------------------------------------------------------------------- /src/images/Profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Profile.png -------------------------------------------------------------------------------- /src/images/Shield3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Shield3.png -------------------------------------------------------------------------------- /src/images/Users4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Users4.png -------------------------------------------------------------------------------- /src/images/Ambulance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Ambulance.png -------------------------------------------------------------------------------- /src/images/BillDollar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/BillDollar.png -------------------------------------------------------------------------------- /src/images/Direction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Direction.png -------------------------------------------------------------------------------- /src/images/Ellipsis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Ellipsis.png -------------------------------------------------------------------------------- /src/images/Library2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/Library2.png -------------------------------------------------------------------------------- /database.rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | ".read": "auth != null", 4 | ".write": "auth != null" 5 | } 6 | } -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | REACT_APP_USE_LOCAL_API=false 2 | REACT_APP_USE_DEV_API=false 3 | REACT_APP_SUPPRESS_ENV_WARNING=0 4 | -------------------------------------------------------------------------------- /src/images/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/marker-icon.png -------------------------------------------------------------------------------- /src/images/climate/cji-map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/climate/cji-map.jpg -------------------------------------------------------------------------------- /src/images/marker-icon-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/marker-icon-2.png -------------------------------------------------------------------------------- /src/images/citylogo-flatblue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/citylogo-flatblue.png -------------------------------------------------------------------------------- /src/images/climate/cji-storymap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/climate/cji-storymap.jpg -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | indent_style = space 7 | indent_size = 2 -------------------------------------------------------------------------------- /src/app/spatial_event_topic_summary/english.js: -------------------------------------------------------------------------------- 1 | export const english = { 2 | of: 'of', 3 | in: 'in', 4 | along: 'along', 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/spatial_event_topic_summary/spanish.js: -------------------------------------------------------------------------------- 1 | export const spanish = { 2 | of: 'de', 3 | in: 'en', 4 | along: 'por', 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/utils.js: -------------------------------------------------------------------------------- 1 | export function capitalizeFirstLetter(string) { 2 | return string.charAt(0).toUpperCase() + string.slice(1); 3 | } 4 | -------------------------------------------------------------------------------- /src/images/climate/resiliency-guide.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/climate/resiliency-guide.jpg -------------------------------------------------------------------------------- /src/styles/components/navbar.scss: -------------------------------------------------------------------------------- 1 | /*JESSE MICHEL 4.6.2018 ALL NAVBAR STYLES*/ 2 | .navbar{ 3 | font-size: 2rem; 4 | background: #f6fcff; 5 | } -------------------------------------------------------------------------------- /src/app/capital_projects/citylogo-419x314.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/app/capital_projects/citylogo-419x314.png -------------------------------------------------------------------------------- /src/images/climate/sustainability-webpage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/images/climate/sustainability-webpage.jpg -------------------------------------------------------------------------------- /src/styles/components/LineGraph.scss: -------------------------------------------------------------------------------- 1 | .disableFrameHover circle { 2 | display: none; 3 | } 4 | 5 | .semiotic-yHoverLine { 6 | stroke-width: 3px; 7 | } -------------------------------------------------------------------------------- /src/styles/components/AreaChart.scss: -------------------------------------------------------------------------------- 1 | svg.annotation-layer-svg g.frame-hover circle { 2 | display: none; 3 | } 4 | 5 | .semiotic-yHoverLine { 6 | stroke-width: 3px; 7 | } -------------------------------------------------------------------------------- /src/app/search/graphql/searchDefaultState.js: -------------------------------------------------------------------------------- 1 | export const defaultSearchState = { 2 | searchText: { 3 | __typename: 'searchText', 4 | search: '', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/search/searchResults/powered_by_google_on_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cityofasheville/simplicity2/HEAD/src/app/search/searchResults/powered_by_google_on_white.png -------------------------------------------------------------------------------- /src/app/capital_projects/CIPTextReplacements.js: -------------------------------------------------------------------------------- 1 | export const CIPTextReplacements = { 2 | "Bond": "Bond 2016", 3 | "CIP": "Capital Improvement Plan", 4 | "Helene": "Helene Recovery", 5 | }; -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_center-block.scss: -------------------------------------------------------------------------------- 1 | // Center-align a block level element 2 | 3 | @mixin center-block() { 4 | display: block; 5 | margin-left: auto; 6 | margin-right: auto; 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/components/search-result-group.scss: -------------------------------------------------------------------------------- 1 | .search-result-group-count{ 2 | background: #9E9E9E; 3 | margin-left: 20px; 4 | } 5 | 6 | .search-result-group-icon{ 7 | margin-right: 20px; 8 | } -------------------------------------------------------------------------------- /src/shared/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NotFound = () => ( 4 |
Not found. Maybe there is a typo in the URL?
5 | ) 6 | 7 | export default NotFound; 8 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_opacity.scss: -------------------------------------------------------------------------------- 1 | // Opacity 2 | 3 | @mixin opacity($opacity) { 4 | opacity: $opacity; 5 | // IE8 filter 6 | $opacity-ie: ($opacity * 100); 7 | filter: alpha(opacity=$opacity-ie); 8 | } 9 | -------------------------------------------------------------------------------- /src/app/search/graphql/searchQueries.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const getSearchText = gql` 4 | query getSearchText { 5 | searchText @client { 6 | search 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/styles/components/PermitsTable.scss: -------------------------------------------------------------------------------- 1 | .table-filter.filter-active { 2 | padding: 3px 5px !important; 3 | border: 2px solid #F39C12 !important; 4 | background-color: #fffae6 !important; 5 | font-weight: 600; 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_size.scss: -------------------------------------------------------------------------------- 1 | // Sizing shortcuts 2 | 3 | @mixin size($width, $height) { 4 | width: $width; 5 | height: $height; 6 | } 7 | 8 | @mixin square($size) { 9 | @include size($size, $size); 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/components/PieChart.scss: -------------------------------------------------------------------------------- 1 | .ordinal-pie-elements { 2 | margin: 0 auto; 3 | text-align: center; 4 | width: 100%; 5 | } 6 | 7 | .pie-container { 8 | width: 100%; 9 | height: 100%; 10 | margin: 0 auto; 11 | } -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_text-overflow.scss: -------------------------------------------------------------------------------- 1 | // Text overflow 2 | // Requires inline-block or block for proper styling 3 | 4 | @mixin text-overflow() { 5 | overflow: hidden; 6 | text-overflow: ellipsis; 7 | white-space: nowrap; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/spatial_event_topic_summary/spatialEventTopicFilters.css: -------------------------------------------------------------------------------- 1 | .filtersDiv { 2 | /* border: 1px solid #ecf0f1; 3 | border-radius: 4px; 4 | padding-top: 15px; 5 | padding-right: 10px; 6 | padding-left: 10px; 7 | margin-bottom: 10px;*/ 8 | } -------------------------------------------------------------------------------- /src/app/capital_projects/CIPColors.js: -------------------------------------------------------------------------------- 1 | export const CIPcolors = { 2 | "Bond 2016": "#0247ae", 3 | "Operating Budget": "#E67125", 4 | "Capital Improvement Plan": "#e65300", 5 | "Bond 2024": "#1597d5", 6 | "Helene Recovery": "#C78800", 7 | }; -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_labels.scss: -------------------------------------------------------------------------------- 1 | // Labels 2 | 3 | @mixin label-variant($color) { 4 | background-color: $color; 5 | 6 | &[href] { 7 | &:hover, 8 | &:focus { 9 | background-color: darken($color, 10%); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_resize.scss: -------------------------------------------------------------------------------- 1 | // Resize anything 2 | 3 | @mixin resizable($direction) { 4 | resize: $direction; // Options: horizontal, vertical, both 5 | overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible` 6 | } 7 | -------------------------------------------------------------------------------- /src/app/search/graphql/searchMutations.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const updateSearchText = gql` 4 | mutation updateSearchText($text: String!) { 5 | updateSearchText(text: $text) @client { 6 | searchText 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /docs/testing/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Simplicity II Testing 4 | --- 5 | 6 | Testing procedures need to be developed. [Interesting background reading for testing principles] (https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d#.qakyax9w3) 7 | -------------------------------------------------------------------------------- /src/styles/components/GranularVolume.scss: -------------------------------------------------------------------------------- 1 | div.rw-widget-picker.rw-widget-container { 2 | height: 1em; 3 | } 4 | 5 | input.rw-input, input.rw-dropdown-list-autofill, input.rw-filter-input { 6 | color: unset; 7 | } 8 | 9 | .dashRows > div { 10 | margin: 0 0 4% 0; 11 | } -------------------------------------------------------------------------------- /src/styles/components/Workflow.scss: -------------------------------------------------------------------------------- 1 | .toggleAbleNode:hover { 2 | cursor: pointer; 3 | } 4 | 5 | .toggleAbleNode circle { 6 | fill: #e6e6e6; 7 | stroke: #e6e6e6; 8 | } 9 | 10 | .toggleAbleNode:hover circle, .toggleAbleNode:focus circle { 11 | stroke: gray; 12 | } 13 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_progress-bar.scss: -------------------------------------------------------------------------------- 1 | // Progress bars 2 | 3 | @mixin progress-bar-variant($color) { 4 | background-color: $color; 5 | 6 | // Deprecated parent class requirement as of v3.2.0 7 | .progress-striped & { 8 | @include gradient-striped; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_text-emphasis.scss: -------------------------------------------------------------------------------- 1 | // Typography 2 | 3 | // [converter] $parent hack 4 | @mixin text-emphasis-variant($parent, $color) { 5 | #{$parent} { 6 | color: $color; 7 | } 8 | a#{$parent}:hover, 9 | a#{$parent}:focus { 10 | color: darken($color, 10%); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | coverage 3 | dist 4 | node_modules 5 | stats.json 6 | yarn-error.log 7 | 8 | # Cruft 9 | .DS_Store 10 | */.DS_Store 11 | npm-debug.log 12 | .idea 13 | 14 | .env 15 | 16 | # Not everyone is using this and eslint covers it anyway 17 | .prettierrc 18 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_reset-filter.scss: -------------------------------------------------------------------------------- 1 | // Reset filters for IE 2 | // 3 | // When you need to remove a gradient background, do not forget to use this to reset 4 | // the IE filter for IE9 and below. 5 | 6 | @mixin reset-filter() { 7 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_nav-divider.scss: -------------------------------------------------------------------------------- 1 | // Horizontal dividers 2 | // 3 | // Dividers (basically an hr) within dropdowns and nav lists 4 | 5 | @mixin nav-divider($color: #e5e5e5) { 6 | height: 1px; 7 | margin: (($line-height-computed / 2) - 1) 0; 8 | overflow: hidden; 9 | background-color: $color; 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_background-variant.scss: -------------------------------------------------------------------------------- 1 | // Contextual backgrounds 2 | 3 | // [converter] $parent hack 4 | @mixin bg-variant($parent, $color) { 5 | #{$parent} { 6 | background-color: $color; 7 | } 8 | a#{$parent}:hover, 9 | a#{$parent}:focus { 10 | background-color: darken($color, 10%); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/search/searchResults/searchResultGroup.css: -------------------------------------------------------------------------------- 1 | .searchResultGroup { 2 | margin-bottom: 15px; 3 | } 4 | 5 | .searchResultGroup h2 { 6 | margin-top: 0px; 7 | font-size: 26px; 8 | color: #4077a5; 9 | } 10 | 11 | .searchResultGroup h2 > i { 12 | margin-right: 8px; 13 | } 14 | 15 | .searchResultGroup h2 span { 16 | margin-left: 8px; 17 | } -------------------------------------------------------------------------------- /src/styles/components/topic-tile.scss: -------------------------------------------------------------------------------- 1 | .topic-tile { 2 | border: 4px solid #ccc; 3 | border-radius: 4px; 4 | margin-bottom: 20px; 5 | padding: 20px; 6 | } 7 | 8 | .clickable-tile:hover, 9 | .clickable-tile:focus, 10 | .clickable-tile:focus-within { 11 | box-shadow: 0 0 .25rem #004987; 12 | z-index: 1; 13 | background-color: #ffffff; 14 | } -------------------------------------------------------------------------------- /src/app/MySimpliCity.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const MySimpliCity = props => ( 5 |
6 |

My SimpliCity

7 | { props.children } 8 |
9 | ); 10 | 11 | MySimpliCity.propTypes = { 12 | children: PropTypes.node, 13 | }; 14 | 15 | export default MySimpliCity; 16 | -------------------------------------------------------------------------------- /src/app/capital_projects/CIPIcons.js: -------------------------------------------------------------------------------- 1 | export const iconDictionary = { 2 | "Transportation & Infrastructure": "bi-bus-front-fill", 3 | "Housing Program": "bi-house-door-fill", 4 | "Parks & Recreation": "bi-tree-fill", 5 | Other: "bi-bookmark-fill", 6 | Water: "bi-droplet-fill", 7 | "Building Construction": "bi-hammer", 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /src/app/development/sla_dashboard/SLADashboardQueries.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const query = gql` 4 | query firstReviewSLA($tasks: [String]) { 5 | firstReviewSLASummary(tasks: $tasks) { 6 | task 7 | met_sla 8 | met_sla_percent 9 | past_sla 10 | month 11 | year 12 | } 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /src/shared/NoResults.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const NoResults = props => ( 5 |
No results found
6 | ); 7 | 8 | NoResults.propTypes = { 9 | 10 | }; 11 | 12 | ButtonGroup.defaultProps = { 13 | // alignment: 'right', 14 | }; 15 | 16 | export default NoResults; 17 | -------------------------------------------------------------------------------- /src/styles/components/treeMap.scss: -------------------------------------------------------------------------------- 1 | .recharts-responsive-container { 2 | clear: both; 3 | }; 4 | 5 | .treeMapBreadcrumb { 6 | color: $coa-medBlue; 7 | font-style: italic; 8 | } 9 | 10 | .treeMapBreadcrumbLink { 11 | cursor: pointer; 12 | text-decoration: underline; 13 | }; 14 | 15 | .treeMapBreadcrumbLink:hover { 16 | text-decoration: none; 17 | }; -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "SimpliCity", 3 | "name": "SimpliCity", 4 | "icons": [{ 5 | "src": "favicon.ico", 6 | "sizes": "64x64 32x32 24x24 16x16", 7 | "type": "image/x-icon" 8 | }], 9 | "start_url": "./index.html", 10 | "display": "standalone", 11 | "theme_color": "#c3ebff", 12 | "background_color": "#c3ebff" 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_alerts.scss: -------------------------------------------------------------------------------- 1 | // Alerts 2 | 3 | @mixin alert-variant($background, $border, $text-color) { 4 | background-color: $background; 5 | border-color: $border; 6 | color: $text-color; 7 | 8 | hr { 9 | border-top-color: darken($border, 5%); 10 | } 11 | .alert-link { 12 | color: darken($text-color, 10%); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/defaultState.js: -------------------------------------------------------------------------------- 1 | import { defaultSearchState } from './app/search/graphql/searchDefaultState'; 2 | import { defaultAuthState } from './utilities/auth/graphql/authDefaultState'; 3 | import { defaultBudgetState } from './app/budget/graphql/budgetDefaultState'; 4 | 5 | export const defaultState = Object.assign({}, defaultSearchState, defaultAuthState, defaultBudgetState); 6 | -------------------------------------------------------------------------------- /src/resolvers.js: -------------------------------------------------------------------------------- 1 | import { searchResolvers } from './app/search/graphql/searchResolvers'; 2 | import { authResolvers } from './utilities/auth/graphql/authResolvers'; 3 | import { budgetResolvers } from './app/budget/graphql/budgetResolvers'; 4 | 5 | export const resolvers = { 6 | Mutation: Object.assign({}, searchResolvers.Mutation, authResolvers.Mutation, budgetResolvers.Mutation), 7 | }; 8 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_tab-focus.scss: -------------------------------------------------------------------------------- 1 | // WebKit-style focus 2 | 3 | @mixin tab-focus() { 4 | // WebKit-specific. Other browsers will keep their default outline style. 5 | // (Initially tried to also force default via `outline: initial`, 6 | // but that seems to erroneously remove the outline in Firefox altogether.) 7 | outline: 5px auto -webkit-focus-ring-color; 8 | outline-offset: -2px; 9 | } 10 | -------------------------------------------------------------------------------- /src/utilities/auth/graphql/authDefaultState.js: -------------------------------------------------------------------------------- 1 | export const defaultAuthState = { 2 | user: { 3 | __typename: 'user', 4 | loggedIn: false, 5 | privilege: 0, 6 | name: '', 7 | email: '', 8 | provider: '', 9 | }, 10 | modal: { 11 | __typename: 'authModal', 12 | open: false, 13 | }, 14 | dropdown: { 15 | __typename: 'authDropdown', 16 | open: false, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_nav-vertical-align.scss: -------------------------------------------------------------------------------- 1 | // Navbar vertical align 2 | // 3 | // Vertically center elements in the navbar. 4 | // Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin. 5 | 6 | @mixin navbar-vertical-align($element-height) { 7 | margin-top: (($navbar-height - $element-height) / 2); 8 | margin-bottom: (($navbar-height - $element-height) / 2); 9 | } 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import 'babel-polyfill'; 4 | 5 | // Import Routes 6 | import Routes from './routes'; 7 | 8 | // Import styles 9 | require('./styles/styles.scss'); 10 | 11 | const container = document.getElementById('root'); 12 | const root = createRoot(container); 13 | root.render(); 14 | 15 | // render( 16 | // (), 17 | // document.getElementById('app'), 18 | // ); 19 | -------------------------------------------------------------------------------- /src/app/Locations.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Locations = () => ( 4 |
5 |

Locations

6 |

Map of Locations: way to search for locations and use map to find locations

7 |

Locations on the map should clickable with a pop-up or panel that 8 | gives some basic details and show a link to the location page

9 |
10 | ); 11 | 12 | Locations.propTypes = {}; 13 | 14 | export default Locations; 15 | -------------------------------------------------------------------------------- /src/app/internal/bpt_projects/ProjectFlowQueries.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const query = gql` 4 | query projects { 5 | projects(after: "2015-01-01", 6 | status: ["Open", "In Progress", "Pending - IT Resolution", "Pending - Vendor", "Waiting On Requestor"]) { 7 | ID 8 | Summary 9 | AssignedTechnician 10 | RequestedDate 11 | Requestor 12 | CurrentStatus 13 | Priority 14 | Notes 15 | } 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function useDebounce({value, delay = 500}) { 4 | const [debouncedValue, setDebouncedValue] = React.useState(value); 5 | 6 | React.useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(handler); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | } 18 | 19 | export default useDebounce; -------------------------------------------------------------------------------- /src/app/search/graphql/searchResolvers.js: -------------------------------------------------------------------------------- 1 | import { getSearchText } from './searchQueries'; 2 | 3 | export const searchResolvers = { 4 | Mutation: { 5 | updateSearchText: (_, { text }, { cache }) => { 6 | const query = getSearchText; 7 | const data = { 8 | searchText: { 9 | __typename: 'searchText', 10 | search: text, 11 | }, 12 | }; 13 | cache.writeQuery({ query, data }); 14 | return data.searchText; 15 | }, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/hooks/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const useLocalStorage = (key, initialValue) => { 4 | const [value, setValue] = useState(() => { 5 | const storedValue = localStorage.getItem(key); 6 | return storedValue ? JSON.parse(storedValue) : initialValue; 7 | }); 8 | 9 | useEffect(() => { 10 | localStorage.setItem(key, JSON.stringify(value)); 11 | }, [key, value]); 12 | 13 | return [value, setValue]; 14 | }; 15 | 16 | export default useLocalStorage; 17 | -------------------------------------------------------------------------------- /src/shared/LinkButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router'; 4 | import Button from './Button'; 5 | 6 | function LinkButton(props) { 7 | return ( 8 | 9 | 21 |
22 | This much data: {props.data.length} 23 |
24 | 25 | , 26 | document.body 27 | ); 28 | 29 | export default DataModal; 30 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_table-row.scss: -------------------------------------------------------------------------------- 1 | // Tables 2 | 3 | @mixin table-row-variant($state, $background) { 4 | // Exact selectors below required to override `.table-striped` and prevent 5 | // inheritance to nested tables. 6 | .table > thead > tr, 7 | .table > tbody > tr, 8 | .table > tfoot > tr { 9 | > td.#{$state}, 10 | > th.#{$state}, 11 | &.#{$state} > td, 12 | &.#{$state} > th { 13 | background-color: $background; 14 | } 15 | } 16 | 17 | // Hover states for `.table-hover` 18 | // Note: this is not available for cells or rows within `thead` or `tfoot`. 19 | .table-hover > tbody > tr { 20 | > td.#{$state}:hover, 21 | > th.#{$state}:hover, 22 | &.#{$state}:hover > td, 23 | &:hover > .#{$state}, 24 | &.#{$state}:hover > th { 25 | background-color: darken($background, 5%); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/components/components.scss: -------------------------------------------------------------------------------- 1 | // @import "home"; 2 | // @import "search"; 3 | // @import "search-bar"; 4 | @import "topic-tile"; 5 | // @import "search-results"; 6 | // @import "search-result-group"; 7 | // @import "search-result"; 8 | @import "multiSelect"; 9 | @import "reactTable"; 10 | @import "treeMap"; 11 | @import "collapsible"; 12 | @import "timeline"; 13 | @import "react-toggle"; 14 | @import "leaflet"; 15 | @import "projects"; 16 | @import "AreaChart"; 17 | @import "DataModal"; 18 | @import "GranularVolume"; 19 | @import "HierarchicalDropdown"; 20 | @import "LineGraph"; 21 | @import "MajorDevelopmentDashboard"; 22 | @import "Permit"; 23 | @import "PieChart"; 24 | @import "TimeSlider"; 25 | @import "Workflow"; 26 | @import "Accordion"; 27 | @import "PermitTimeline"; 28 | @import "DetailsGrouping"; 29 | @import "DetailsTable"; 30 | @import "TopicCard"; 31 | @import "PermitsTable"; -------------------------------------------------------------------------------- /src/app/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import Search from './search/Search'; 5 | import SuggestSearch from './search/SuggestSearch'; 6 | import SuggestSearchWrapper from './search/SuggestSearchWrapper'; 7 | import Topics from './Topics'; 8 | import GetVersion from '../shared/GetVersion' 9 | 10 | function Homepage(props) { 11 | return ( 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 | ); 21 | } 22 | 23 | Homepage.propTypes = { 24 | topics: PropTypes.arrayOf(PropTypes.string), 25 | }; 26 | 27 | Homepage.defaultProps = { 28 | topics: [ 29 | 'BUDGET', 30 | 'CAPITAL_PROJECTS', 31 | 'CRIME', 32 | 'DEVELOPMENT', 33 | // 'HOMELESSNESS', 34 | ], 35 | }; 36 | 37 | export default Homepage; 38 | -------------------------------------------------------------------------------- /src/shared/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class ErrorBoundary extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false }; 7 | } 8 | 9 | static getDerivedStateFromError(error) { 10 | // Update state so the next render will show the fallback UI. 11 | return { hasError: true }; 12 | } 13 | 14 | componentDidCatch(error, info) { 15 | // You can also log the error to an error reporting service 16 | console.log(error, info); 17 | } 18 | 19 | render() { 20 | if (this.state.hasError) { 21 | // You can render any custom fallback UI 22 | return

Oops, something went wrong! Try refreshing the page. If you tried that and it did not work, please email help@ashevillenc.gov.

; 23 | } 24 | 25 | return this.props.children; 26 | } 27 | } 28 | 29 | export default ErrorBoundary; 30 | -------------------------------------------------------------------------------- /src/styles/components/Permit.scss: -------------------------------------------------------------------------------- 1 | .permit-form-group { 2 | width: 100%; 3 | padding: 1em; 4 | background-color: #f2f2f2; 5 | border-radius: 2px; 6 | margin: 2px 1rem 2px 0; 7 | display: flex; 8 | justify-content: space-between; 9 | } 10 | 11 | .permit-form-group .display-label, .permit-form-group.bool { 12 | font-weight: 500; 13 | } 14 | 15 | .permit-icon-boolean svg { 16 | margin-right: 0.5em; 17 | } 18 | 19 | .permit-map-container { 20 | min-height: 250px; 21 | } 22 | 23 | .display-label { 24 | margin-right: 1rem; 25 | } 26 | 27 | @media(max-width: 767px) { 28 | .permit-map-container { 29 | height: 300px; 30 | } 31 | .permit-form-group { 32 | display: block; 33 | } 34 | .permit-form-group .display-label, .permit-form-group .formatted-val { 35 | width: 100%; 36 | } 37 | } 38 | 39 | @media(min-width: 767px) { 40 | .permit-map-row { 41 | display: flex; 42 | align-items: stretch; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/shared/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import moment from 'moment'; 4 | 5 | const Error = props => ( 6 |
7 |
8 |
9 |

10 | There was an error reaching the server. You may report issues using this form. 11 |

12 |

13 | Time: {moment().format('M/DD/YYYY HH:mm:ss Z')} UTC 14 |

15 |

16 | Error details: {props.message} 17 |

18 |
19 |
20 |
21 | ); 22 | 23 | Error.propTypes = { 24 | message: PropTypes.string, 25 | }; 26 | 27 | export default Error; 28 | -------------------------------------------------------------------------------- /src/utilities/auth/graphql/authMutations.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const updateUser = gql` 4 | mutation updateUser( 5 | $loggedIn: Boolean 6 | $privilege: Int 7 | $name: String 8 | $email: String 9 | $provider: String 10 | ) { 11 | updateUser( 12 | loggedIn: $loggedIn 13 | privilege: $privilege 14 | name: $name 15 | email: $email 16 | provider: $provider 17 | ) @client { 18 | loggedIn 19 | privilege 20 | name 21 | email 22 | provider 23 | } 24 | } 25 | `; 26 | 27 | export const updateAuthModal = gql` 28 | mutation updateAuthModal($open: Boolean) { 29 | updateAuthModal(open: $open) @client { 30 | modal 31 | } 32 | } 33 | `; 34 | 35 | export const updateAuthDropdown = gql` 36 | mutation updateAuthDropdown($open: Boolean) { 37 | updateAuthDropdown(open: $open) @client { 38 | dropdown 39 | } 40 | } 41 | `; 42 | 43 | -------------------------------------------------------------------------------- /src/app/development/trc/PermitTypeCards.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PermitTypeCard from './PermitTypeCard'; 3 | import { trcProjectTypes } from './textContent'; 4 | 5 | const PermitTypeCards = () => { 6 | let cardWidth = '40%'; 7 | if (window.innerWidth < 500) { 8 | cardWidth = '90%'; 9 | } 10 | return ( 11 |
19 | {Object.keys(trcProjectTypes).map(type => ( 20 |
30 | 31 |
32 | ))} 33 |
34 | ); 35 | }; 36 | 37 | export default PermitTypeCards; 38 | -------------------------------------------------------------------------------- /src/app/development/volume/StatusDash.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import LoadingAnimation from '../../../shared/LoadingAnimation'; 4 | import StatusDistributionMultiples from './StatusDistributionMultiples'; 5 | 6 | 7 | const StatusDash = (props) => { 8 | const title = props.selectedHierarchyTitle ? 9 | `Record Status Distributions by ${props.selectedHierarchyTitle}` : 10 | 'Record Status Distributions'; 11 | 12 | return (
13 |
14 |

{title}

15 | {props.selectedNodes ? 16 | ( !node.othered)} 21 | />) : 22 | 23 | } 24 |
25 |
); 26 | } 27 | 28 | export default StatusDash; 29 | -------------------------------------------------------------------------------- /src/styles/bootstrap/_close.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Close icons 3 | // -------------------------------------------------- 4 | 5 | 6 | .close { 7 | float: right; 8 | font-size: ($font-size-base * 1.5); 9 | font-weight: $close-font-weight; 10 | line-height: 1; 11 | color: $close-color; 12 | text-shadow: $close-text-shadow; 13 | @include opacity(.2); 14 | 15 | &:hover, 16 | &:focus { 17 | color: $close-color; 18 | text-decoration: none; 19 | cursor: pointer; 20 | @include opacity(.5); 21 | } 22 | 23 | // [converter] extracted button& to button.close 24 | } 25 | 26 | // Additional properties for button version 27 | // iOS requires the button element instead of an anchor tag. 28 | // If you want the anchor version, it requires `href="#"`. 29 | // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile 30 | button.close { 31 | padding: 0; 32 | cursor: pointer; 33 | background: transparent; 34 | border: 0; 35 | -webkit-appearance: none; 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/components/TimeSlider.scss: -------------------------------------------------------------------------------- 1 | .brushedChart rect.selection { 2 | fill: darken(#f6fcff, 10%); 3 | stroke: darken(#f6fcff, 30%) 4 | } 5 | 6 | .brushedChart rect.handle { 7 | fill: darken(#f6fcff, 50%); 8 | } 9 | 10 | input[type="date"]::-webkit-clear-button { 11 | display: none; 12 | } 13 | 14 | .timepicker-dropdown { 15 | display: flex; 16 | flex-wrap: wrap; 17 | } 18 | 19 | .timepicker-dropdown .timepicker-input-item { 20 | align-content: stretch; 21 | align-items: center; 22 | margin: 0.25rem; 23 | white-space: nowrap; 24 | flex-grow: 1; 25 | } 26 | 27 | .timepicker-dropdown div.timepicker-input-item { 28 | display: flex; 29 | } 30 | 31 | 32 | .timepicker-dropdown .timepicker-input-item label { 33 | margin: 0 0.25rem; 34 | } 35 | 36 | @media(max-width: 767px) { 37 | .timepicker-dropdown { 38 | text-align: left; 39 | } 40 | 41 | .timepicker-dropdown .timepicker-input-item { 42 | width: 100%; 43 | } 44 | } 45 | 46 | @media(min-width: 767px) {} -------------------------------------------------------------------------------- /src/styles/components/filterCheckbox.scss: -------------------------------------------------------------------------------- 1 | /*.filterCheckbox { 2 | border-radius: 1px; 3 | border: #16abe4 solid 2px;; 4 | margin-bottom: 10px; 5 | } 6 | 7 | .filterCheckboxDisabled { 8 | border-radius: 2px; 9 | border: #ffffff solid 2px; 10 | margin-bottom: 10px; 11 | opacity: 0.25; 12 | } 13 | 14 | .disabledCursor { 15 | cursor: not-allowed; 16 | } 17 | 18 | .unchecked { 19 | border-radius: 2px; 20 | margin-bottom: 10px; 21 | border: #ffffff solid 2px; 22 | opacity: .75; 23 | } 24 | 25 | label { 26 | font-weight: normal; 27 | } 28 | 29 | .unchecked input[type="checkbox"]:focus + label, 30 | .filterCheckboxDisabled input[type="checkbox"]:focus + label, 31 | .filterCheckbox input[type="checkbox"]:focus + label, 32 | .checkbox-inline input[type="checkbox"]:focus + label{ 33 | outline: solid 2px #4579B3; 34 | outline-offset: 3px; 35 | } 36 | 37 | .backgroundChecked { 38 | background-color: #d3f1ff; 39 | } 40 | .backgroundUnchecked { 41 | background-color: #eeeeee; 42 | }*/ -------------------------------------------------------------------------------- /src/styles/bootstrap/_component-animations.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Component animations 3 | // -------------------------------------------------- 4 | 5 | // Heads up! 6 | // 7 | // We don't use the `.opacity()` mixin here since it causes a bug with text 8 | // fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. 9 | 10 | .fade { 11 | opacity: 0; 12 | @include transition(opacity .15s linear); 13 | &.in { 14 | opacity: 1; 15 | } 16 | } 17 | 18 | .collapse { 19 | display: none; 20 | 21 | &.in { display: block; } 22 | // [converter] extracted tr&.in to tr.collapse.in 23 | // [converter] extracted tbody&.in to tbody.collapse.in 24 | } 25 | 26 | tr.collapse.in { display: table-row; } 27 | 28 | tbody.collapse.in { display: table-row-group; } 29 | 30 | .collapsing { 31 | position: relative; 32 | height: 0; 33 | overflow: hidden; 34 | @include transition-property(height, visibility); 35 | @include transition-duration(.35s); 36 | @include transition-timing-function(ease); 37 | } 38 | -------------------------------------------------------------------------------- /src/shared/visualization/Tooltip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | 5 | const Tooltip = (props) => { 6 | const styles = props.style || {}; 7 | styles.padding = '0.35rem'; 8 | styles.letterSpacing = '0.015rem' 9 | const minWidth = Math.min( 10 | (props.textLines.map(line => line.text).join('').length + 1) / props.textLines.length, 11 | 10 12 | ); 13 | styles.minWidth = `${minWidth}em`; 14 | return ( 15 |
16 | {props.title} 17 |
18 | {props.textLines.map((lineObj, i) => 19 |
{lineObj.text}
20 | )} 21 |
); 22 | }; 23 | 24 | Tooltip.propTypes = { 25 | textLines: PropTypes.arrayOf(PropTypes.object), 26 | title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 27 | }; 28 | 29 | Tooltip.defaultProps = { 30 | textLines: [], 31 | title: '', 32 | }; 33 | 34 | export default Tooltip; 35 | -------------------------------------------------------------------------------- /src/shared/DropDownButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const ButtonDropdown = props => ( 5 |
6 | 10 | 17 |
18 | ); 19 | 20 | ButtonDropdown.propTypes = { 21 | // children: PropTypes.node, 22 | // alignment: PropTypes.string, 23 | // style: PropTypes.object, // eslint-disable-line 24 | }; 25 | 26 | ButtonDropdown.defaultProps = { 27 | // alignment: 'right', 28 | }; 29 | 30 | export default ButtonGroup; 31 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/styles/components/TopicCard.scss: -------------------------------------------------------------------------------- 1 | .topicCard { 2 | /*border: 2px solid #ddd;*/ 3 | border-radius: 5px; 4 | padding-bottom: 10px; 5 | padding-top: 10px; 6 | margin-bottom: 10px; 7 | background: #fbfbfb; 8 | } 9 | 10 | .topicCard i { 11 | padding-bottom: 5px; 12 | width: 100%; 13 | } 14 | 15 | .row.topic-options { 16 | margin-top: 21px; 17 | display: flex; 18 | justify-content: space-evenly; 19 | } 20 | 21 | .topicCard .topic-card { 22 | text-decoration: none; 23 | } 24 | 25 | .topicCard .topic-card:focus { 26 | outline: none; 27 | text-decoration: underline; 28 | } 29 | 30 | .topicCard { 31 | box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .075); 32 | min-height: 100%; 33 | } 34 | 35 | .topicCard:hover, 36 | .topicCard:focus, 37 | .topicCard:focus-within { 38 | box-shadow: 0 0 .15rem #004987; 39 | z-index: 1; 40 | } 41 | 42 | .topicCard a:focus, 43 | .topicCard:hover a { 44 | text-decoration: underline; 45 | } 46 | 47 | .aligned-row { 48 | display: flex; 49 | flex-flow: row wrap; 50 | 51 | &::before { 52 | display: block; 53 | } 54 | } -------------------------------------------------------------------------------- /src/shared/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Checkbox extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { checked: props.checked }; 9 | this.label = props.label; 10 | this.value = props.value; 11 | this.toggleChecked = this.toggleChecked.bind(this); 12 | this.onChangeCallback = props.onChangeCallback; 13 | } 14 | 15 | toggleChecked() { 16 | this.setState({ 17 | checked: !this.state.checked, 18 | }); 19 | if (this.props.onChangeCallback !== undefined) { 20 | this.props.onChangeCallback(!this.state.checked); 21 | } 22 | } 23 | 24 | render() { 25 | return ( 26 | 27 | ); 28 | } 29 | } 30 | 31 | Checkbox.propTypes = { 32 | label: PropTypes.string, 33 | value: PropTypes.string, 34 | checked: PropTypes.bool, 35 | onChangeCallback: PropTypes.func, 36 | }; 37 | 38 | export default Checkbox; 39 | -------------------------------------------------------------------------------- /src/styles/bootstrap/_thumbnails.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Thumbnails 3 | // -------------------------------------------------- 4 | 5 | 6 | // Mixin and adjust the regular image class 7 | .thumbnail { 8 | display: block; 9 | padding: $thumbnail-padding; 10 | margin-bottom: $line-height-computed; 11 | line-height: $line-height-base; 12 | background-color: $thumbnail-bg; 13 | border: 1px solid $thumbnail-border; 14 | border-radius: $thumbnail-border-radius; 15 | @include transition(border .2s ease-in-out); 16 | 17 | > img, 18 | a > img { 19 | @include img-responsive; 20 | margin-left: auto; 21 | margin-right: auto; 22 | } 23 | 24 | // [converter] extracted a&:hover, a&:focus, a&.active to a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active 25 | 26 | // Image captions 27 | .caption { 28 | padding: $thumbnail-caption-padding; 29 | color: $thumbnail-caption-color; 30 | } 31 | } 32 | 33 | // Add a hover state for linked versions only 34 | a.thumbnail:hover, 35 | a.thumbnail:focus, 36 | a.thumbnail.active { 37 | border-color: $link-color; 38 | } 39 | -------------------------------------------------------------------------------- /src/app/development/trc/LargeNodeWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import LargeNodeContents from './LargeNodeContents'; 4 | 5 | /* 6 | Because LargeNodeContents is used both in the diagram and as a modal, the wrapper which positions it in the SVG had to be separated out. 7 | 8 | https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject 9 | */ 10 | const LargeNodeWrapper = ({ node, yOffset, edgeStroke }) => ( 11 | 19 | 20 | 21 | ); 22 | 23 | LargeNodeWrapper.propTypes = { 24 | node: PropTypes.shape({}).isRequired, 25 | yOffset: PropTypes.number.isRequired, 26 | edgeStroke: PropTypes.number, 27 | }; 28 | 29 | LargeNodeWrapper.defaultProps = { 30 | edgeStroke: 3, 31 | }; 32 | 33 | export default LargeNodeWrapper; 34 | -------------------------------------------------------------------------------- /src/styles/components/search-bar.scss: -------------------------------------------------------------------------------- 1 | .search-icon-in-search { 2 | position: absolute; 3 | right: 70px; 4 | top: 14px; 5 | font-size: 2em !important; 6 | } 7 | 8 | .icon-in-search { 9 | position: absolute; 10 | right: 35px; 11 | top: 14px; 12 | font-size: 2em !important; 13 | } 14 | 15 | .icon-in-search:hover { 16 | position: absolute; 17 | right: 35px; 18 | top: 14px; 19 | font-size: 2em !important; 20 | color: darken($brand-primary, 20%); 21 | cursor: pointer; 22 | } 23 | 24 | .search-bar { 25 | height: 60px; 26 | font-size: 30px; 27 | box-shadow: 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12), 0 2px 4px -1px rgba(0,0,0,0.3); 28 | } 29 | 30 | .search-bar::-webkit-input-placeholder { 31 | font-size: 30px; 32 | } 33 | 34 | 35 | @media (max-width: 700px) { 36 | .icon-in-search { 37 | position: absolute; 38 | right: 25px; 39 | top: 13px; 40 | font-size: 1em !important; 41 | } 42 | 43 | .search-bar { 44 | height: 40px; 45 | font-size: 18px; 46 | } 47 | 48 | .search-bar::-webkit-input-placeholder { 49 | font-size: 18px; 50 | } 51 | } -------------------------------------------------------------------------------- /src/styles/bootstrap/_pager.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Pager pagination 3 | // -------------------------------------------------- 4 | 5 | 6 | .pager { 7 | padding-left: 0; 8 | margin: $line-height-computed 0; 9 | list-style: none; 10 | text-align: center; 11 | @include clearfix; 12 | li { 13 | display: inline; 14 | > a, 15 | > span { 16 | display: inline-block; 17 | padding: 5px 14px; 18 | background-color: $pager-bg; 19 | border: 1px solid $pager-border; 20 | border-radius: $pager-border-radius; 21 | } 22 | 23 | > a:hover, 24 | > a:focus { 25 | text-decoration: none; 26 | background-color: $pager-hover-bg; 27 | } 28 | } 29 | 30 | .next { 31 | > a, 32 | > span { 33 | float: right; 34 | } 35 | } 36 | 37 | .previous { 38 | > a, 39 | > span { 40 | float: left; 41 | } 42 | } 43 | 44 | .disabled { 45 | > a, 46 | > a:hover, 47 | > a:focus, 48 | > span { 49 | color: $pager-disabled-color; 50 | background-color: $pager-bg; 51 | cursor: $cursor-disabled; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /docs/deployment/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | # How the site works 5 | 6 | The SimpliCity frontend lives in AWS (Amazon Web Services). It deploys to an Amplify application, with environments built from various branches in this repo (development, production, and temporary feature branches). https for the [production environment](https://simplicity.ashevillenc.gov/) is enabled through a CloudFront distribution pointing to the production Amplify environment. The production domain is managed in AWS Route 53. The https certificate provided by the AWS certificate manager. 7 | 8 | ## Updates / Deployment 9 | 10 | Updates to the site content require: 11 | * Push new code to Github 12 | * NOTE: Amplify watches certain branches in the repo and deployments are triggered by new commits to those branches (development and production, plus any temporary feature branch environments) 13 | * Invalidate the files in Cloudfront (Optional, only needed if deployed changes are not visible) 14 | * Go to the Cloudfront distribution in the AWS console. Click on the "Invalidations" tab. "Create Invalidation" and invalidate /* as the Object Paths. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Stories in Ready](https://badge.waffle.io/cityofasheville/simplicity2.png?label=ready&title=Ready)](https://waffle.io/cityofasheville/simplicity2) 2 | # SimpliCity II 3 | 4 | ## Getting started 5 | 6 | You'll need Node (v14, see .nvmrc) and npm installed, then run the following commands to download the repository, install dependencies, and launch a dev server: 7 | 8 | 1. `git clone https://github.com/cityofasheville/simplicity2.git simplicity2` 9 | 2. `cd simplicity2` 10 | 3. `npm install` 11 | 4. `npm start` 12 | 13 | ## Documentation 14 | 15 | The documentation is maintained in the [docs directory](./docs). 16 | 17 | ## License 18 | 19 | This project is licensed under the MIT license. For more information see the [license file](./LICENSE.md). 20 | 21 | ## Code of Conduct 22 | 23 | We have adopted the Contributor Covenant. See our [code of conduct file](./CODE_OF_CONDUCT.md) for more information. 24 | 25 | ## Acknowledgments 26 | 27 | The first SimpliCity, which is the inspiration for SimpliCity 2, was originally developed by [Cameron Carlyle](https://github.com/carlyleec) and [Dave Michelson](https://github.com/daveism). 28 | -------------------------------------------------------------------------------- /src/app/budget/graphql/budgetMutations.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const updateSankeyData = gql` 4 | mutation updateSankeyData($sankeyData: sankeyData, ) { 5 | updateSankeyData(sankeyData: $sankeyData) @client { 6 | nodes 7 | links 8 | } 9 | } 10 | `; 11 | 12 | export const updateBudgetTrees = gql` 13 | mutation updateBudgetTrees($budgetTrees: budgetTrees) { 14 | updateBudgetTrees(budgetTrees: $budgetTrees) @client { 15 | expenseTree 16 | revenueTree 17 | expenseTreeForTreemap 18 | revenueTreeForTreemap 19 | } 20 | } 21 | `; 22 | 23 | export const updateBudgetSummaryUse = gql` 24 | mutation updateBudgetSummaryUse($budgetSummaryUse: budgetSummaryUse) { 25 | updateBudgetSummaryUse(budgetSummaryUse: $budgetSummaryUse) @client { 26 | dataKeys 27 | dataValues 28 | } 29 | } 30 | `; 31 | 32 | export const updateBudgetSummaryDept = gql` 33 | mutation updateBudgetSummaryDept($budgetSummaryDept: budgetSummaryDept) { 34 | updateBudgetSummaryDept(budgetSummaryDept: $budgetSummaryDept) @client { 35 | dataKeys 36 | dataValues 37 | } 38 | } 39 | `; 40 | -------------------------------------------------------------------------------- /src/app/crime/english.js: -------------------------------------------------------------------------------- 1 | export const english = { 2 | back: 'Back', 3 | case_no: 'Case #', 4 | city_block: 'a city block (110 yards)', 5 | couple_blocks: 'a couple city blocks (1/8 mile)', 6 | quarter_mile: 'a quarter mile', 7 | half_mile: 'a half mile', 8 | mile: 'a mile', 9 | chart_view: 'Chart', 10 | click_to_crime: 'Click to crime in map', 11 | crime: 'Crime', 12 | crimes: 'Crimes', 13 | crimes_by_address_filename: 'crimes_by_address.csv', 14 | crimes_by_street_filename: 'crimes_by_street.csv', 15 | crimes_by_neighborhood_filename: 'crimes_by_neighborhood', 16 | crime_pie_chart: 'Crime pie chart', 17 | date: 'Date', 18 | during: 'during', 19 | last_30_days: 'the last 30 days', 20 | last_6_months: 'the last 6 months', 21 | last_year: 'the last year', 22 | last_5_years: 'the last 5 years', 23 | all_time: 'all time', 24 | law_beat: 'Law Beat', 25 | list_view: 'List view', 26 | location: 'Location', 27 | map_view: 'Map view', 28 | no_results_found: 'No results found', 29 | placeholder: 'Search...', 30 | type: 'Type', 31 | view: 'view', 32 | view_apd_reports: 'View APD reports', 33 | within: 'within', 34 | }; -------------------------------------------------------------------------------- /src/styles/bootstrap/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------------------------------- 3 | 4 | // Utilities 5 | @import "mixins/hide-text"; 6 | @import "mixins/opacity"; 7 | @import "mixins/image"; 8 | @import "mixins/labels"; 9 | @import "mixins/reset-filter"; 10 | @import "mixins/resize"; 11 | @import "mixins/responsive-visibility"; 12 | @import "mixins/size"; 13 | @import "mixins/tab-focus"; 14 | @import "mixins/reset-text"; 15 | @import "mixins/text-emphasis"; 16 | @import "mixins/text-overflow"; 17 | @import "mixins/vendor-prefixes"; 18 | 19 | // Components 20 | @import "mixins/alerts"; 21 | @import "mixins/buttons"; 22 | @import "mixins/panels"; 23 | @import "mixins/pagination"; 24 | @import "mixins/list-group"; 25 | @import "mixins/nav-divider"; 26 | @import "mixins/forms"; 27 | @import "mixins/progress-bar"; 28 | @import "mixins/table-row"; 29 | 30 | // Skins 31 | @import "mixins/background-variant"; 32 | @import "mixins/border-radius"; 33 | @import "mixins/gradients"; 34 | 35 | // Layout 36 | @import "mixins/clearfix"; 37 | @import "mixins/center-block"; 38 | @import "mixins/nav-vertical-align"; 39 | @import "mixins/grid-framework"; 40 | @import "mixins/grid"; 41 | -------------------------------------------------------------------------------- /src/utilities/auth/graphql/authResolvers.js: -------------------------------------------------------------------------------- 1 | import { getUser, getModalOpen, getDropdownOpen } from './authQueries'; 2 | 3 | export const authResolvers = { 4 | Mutation: { 5 | updateUser: (_, { loggedIn, privilege, name, email, provider }, { cache }) => { 6 | const data = { 7 | user: { 8 | __typename: 'user', 9 | loggedIn, 10 | privilege, 11 | name, 12 | email, 13 | provider, 14 | }, 15 | }; 16 | cache.writeQuery({ query: getUser, data }); 17 | return data.user; 18 | }, 19 | updateAuthModal: (_, { open }, { cache }) => { 20 | const data = { 21 | modal: { 22 | __typename: 'authModal', 23 | open, 24 | }, 25 | }; 26 | cache.writeQuery({ query: getModalOpen, data }); 27 | return data.modal; 28 | }, 29 | updateAuthDropdown: (_, { open }, { cache }) => { 30 | const data = { 31 | dropdown: { 32 | __typename: 'authDropdown', 33 | open, 34 | }, 35 | }; 36 | cache.writeQuery({ query: getDropdownOpen, data }); 37 | return data.dropdown; 38 | }, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/styles/bootstrap/_utilities.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Utility classes 3 | // -------------------------------------------------- 4 | 5 | 6 | // Floats 7 | // ------------------------- 8 | 9 | .clearfix { 10 | @include clearfix; 11 | } 12 | .center-block { 13 | @include center-block; 14 | } 15 | .pull-right { 16 | float: right !important; 17 | } 18 | .pull-left { 19 | float: left !important; 20 | } 21 | 22 | 23 | // Toggling content 24 | // ------------------------- 25 | 26 | // Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1 27 | .hide { 28 | display: none !important; 29 | } 30 | .show { 31 | display: block !important; 32 | } 33 | .invisible { 34 | visibility: hidden; 35 | } 36 | .text-hide { 37 | @include text-hide; 38 | } 39 | 40 | 41 | // Hide from screenreaders and browsers 42 | // 43 | // Credit: HTML5 Boilerplate 44 | 45 | .hidden { 46 | display: none !important; 47 | } 48 | 49 | 50 | // For Affix plugin 51 | // ------------------------- 52 | 53 | .affix { 54 | position: fixed; 55 | } 56 | 57 | .offscreen { 58 | position: absolute; 59 | left: 0; 60 | top: -500px; 61 | width: 1px; 62 | height: 1px; 63 | overflow: hidden; 64 | } -------------------------------------------------------------------------------- /src/app/development/trc/NodeSteps.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { whoIcons } from './textContent'; 3 | 4 | const NodeSteps = ({ steps, nodeId }) => ( 5 | 34 | ); 35 | 36 | export default NodeSteps; 37 | -------------------------------------------------------------------------------- /src/styles/bootstrap/_media.scss: -------------------------------------------------------------------------------- 1 | .media { 2 | // Proper spacing between instances of .media 3 | margin-top: 15px; 4 | 5 | &:first-child { 6 | margin-top: 0; 7 | } 8 | } 9 | 10 | .media, 11 | .media-body { 12 | zoom: 1; 13 | overflow: hidden; 14 | } 15 | 16 | .media-body { 17 | width: 10000px; 18 | } 19 | 20 | .media-object { 21 | display: block; 22 | 23 | // Fix collapse in webkit from max-width: 100% and display: table-cell. 24 | &.img-thumbnail { 25 | max-width: none; 26 | } 27 | } 28 | 29 | .media-right, 30 | .media > .pull-right { 31 | padding-left: 10px; 32 | } 33 | 34 | .media-left, 35 | .media > .pull-left { 36 | padding-right: 10px; 37 | } 38 | 39 | .media-left, 40 | .media-right, 41 | .media-body { 42 | display: table-cell; 43 | vertical-align: top; 44 | } 45 | 46 | .media-middle { 47 | vertical-align: middle; 48 | } 49 | 50 | .media-bottom { 51 | vertical-align: bottom; 52 | } 53 | 54 | // Reset margins on headings for tighter default spacing 55 | .media-heading { 56 | margin-top: 0; 57 | margin-bottom: 5px; 58 | } 59 | 60 | // Media list variation 61 | // 62 | // Undo default ul/ol styles 63 | .media-list { 64 | padding-left: 0; 65 | list-style: none; 66 | } 67 | -------------------------------------------------------------------------------- /src/utilities/lang/LangSwitcher.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withLanguage } from './LanguageContext'; 3 | 4 | const LangSwitcher = props => ( 5 |
  • 6 | props.language.switchLanguage(props.language.language, props.language.label, !props.language.dropdownOpen)} 9 | data-toggle="dropdown" 10 | role="button" 11 | aria-haspopup="true" 12 | aria-expanded="false" 13 | >{props.language.label} 14 | 15 | 16 | 34 |
  • 35 | ); 36 | 37 | export default withLanguage(LangSwitcher); 38 | 39 | -------------------------------------------------------------------------------- /src/shared/LinkFocusWrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class LinkFocusWrapper extends React.Component { 5 | thisRef = React.createRef(); 6 | 7 | onLinkFocus = (e) => { 8 | if (e.target === this.thisRef.current) { 9 | e.preventDefault(); 10 | e.stopPropagation(); 11 | this.thisRef.current.firstChild.focus(); 12 | } 13 | }; 14 | 15 | attachRef = (el) => { 16 | this.thisRef.current = el; 17 | 18 | if (typeof this.props.focusRef === 'function') { 19 | this.props.focusRef(el); 20 | } else { 21 | this.props.focusRef.current = el; 22 | } 23 | }; 24 | 25 | render() { 26 | const { Component } = this.props; 27 | return ( 28 | 29 | {this.props.children} 30 | 31 | ); 32 | } 33 | } 34 | 35 | LinkFocusWrapper.propTypes = { 36 | focusRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired, 37 | children: PropTypes.node.isRequired, 38 | Component: PropTypes.node, 39 | }; 40 | 41 | LinkFocusWrapper.defaultProps = { 42 | Component: 'div', 43 | }; 44 | 45 | export default LinkFocusWrapper; 46 | -------------------------------------------------------------------------------- /src/shared/visualization/BigNumber.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Query } from 'react-apollo'; 4 | 5 | const BigNumber = props => ( 6 | 10 | {({ loading, error, data }) => { 11 | if (loading) return (
    Loading...
    ) 12 | let val = '' 13 | if (error) { 14 | val = 'Error' 15 | } 16 | val = props.aggregateFunction(data) 17 | return (
    18 |
    24 |
    29 | {val} 30 |
    31 |
    32 | {props.label} 33 |
    34 |
    35 |
    ) 36 | }} 37 |
    38 | ); 39 | 40 | BigNumber.propTypes = { 41 | // query: PropTypes.object, 42 | label: PropTypes.string, 43 | } 44 | 45 | BigNumber.defaultProps = { 46 | // query: '', 47 | label: '', 48 | } 49 | 50 | export default BigNumber; 51 | -------------------------------------------------------------------------------- /src/styles/components/collapsible.scss: -------------------------------------------------------------------------------- 1 | /*.Collapsible { 2 | background-color: $coa-medBlue; 3 | color: white; 4 | margin-bottom: 4px; 5 | border: 1px solid transparent; 6 | border-radius: 2px; 7 | } 8 | 9 | .Collapsible__contentInner { 10 | background-color: white; 11 | color: $text-color; 12 | padding-top: 10px; 13 | padding-bottom: 0px; 14 | padding-left: $panel-body-padding; 15 | padding-right: $panel-body-padding; 16 | @include clearfix; 17 | } 18 | 19 | .Collapsible__trigger { 20 | padding-top: 5px; 21 | cursor: pointer; 22 | padding: $panel-heading-padding; 23 | border-bottom: 1px solid transparent; 24 | @include border-top-radius(($panel-border-radius - 1)); 25 | 26 | > .dropdown .dropdown-toggle { 27 | color: inherit; 28 | } 29 | } 30 | 31 | .Collapsible__trigger.is-closed::before { 32 | content: ' +'; 33 | background-color: #fff; 34 | color: #4077a5; 35 | padding-right: 5px; 36 | padding-left: 5px; 37 | margin-right: 5px; 38 | border-radius: 7px; 39 | } 40 | 41 | .Collapsible__trigger.is-open::before { 42 | content: '-'; 43 | background-color: #fff; 44 | color: #4077a5; 45 | padding-right: 7px; 46 | padding-left: 7px; 47 | margin-right: 5px; 48 | border-radius: 7px; 49 | }*/ -------------------------------------------------------------------------------- /src/app/crime/spanish.js: -------------------------------------------------------------------------------- 1 | export const spanish = { 2 | back: 'Volver', 3 | case_no: 'N\xFAmero de caso', 4 | chart_view: 'Gr\xE1fico', 5 | click_to_crime: 'Clic a cr\xEDmen en mapa', 6 | city_block: 'un bloque (110 yardas)', 7 | couple_blocks: 'un par de bloques (1/8 milla)', 8 | quarter_mile: 'un cuarto de milla', 9 | half_mile: 'una media milla', 10 | mile: 'una milla', 11 | crime: 'Crimen', 12 | crimes: 'Cr\xEDmenes', 13 | crimes_by_address_filename: 'crimenes_alrededor_de_la_direccion.csv', 14 | crimes_by_street_filename: 'crimenes_por_la_calle.csv', 15 | crimes_by_neighborhood_filename: 'crimenes_en_el_barrio.csv', 16 | crime_pie_chart: 'Gr\xE1fico circular de cr\xEDmenes', 17 | date: 'Fecha', 18 | during: 'durante', 19 | last_30_days: 'los \xFAltimos 30 d\xEDas', 20 | last_6_months: 'los \xFAltimos 6 meses', 21 | last_year: 'el \xFAltimo a\xF1o', 22 | last_5_years: 'los \xFAltimos 5 a\xF1os', 23 | all_time: 'todo el tiempo', 24 | law_beat: 'Ronda', 25 | list_view: 'Lista', 26 | location: 'Lugar', 27 | map_view: 'Mapa', 28 | no_results_found: 'No se han encontrado resultados', 29 | placeholder: 'Buscar...', 30 | type: 'Tipo', 31 | view: 'ver', 32 | view_apd_reports: 'Ver informes del APD', 33 | within: 'dentro de', 34 | }; 35 | -------------------------------------------------------------------------------- /src/app/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from '../shared/Icon'; 3 | import { IM_GITHUB } from '../shared/iconConstants'; 4 | 5 | const Footer = () => ( 6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 | We strive for full accessibility. Report issues with this website through our 13 | feedback form 19 | . 20 |
    21 |
    It's open source! Fork it on GitHub
    22 |
    23 |
    24 |
    25 |
    26 | ); 27 | 28 | export default Footer; 29 | -------------------------------------------------------------------------------- /src/shared/DetailsFormGroup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const DetailsFormGroup = props => ( 5 |
    8 |
    9 |
    10 | {props.icon !== null && 11 | {props.icon} 12 | } 13 | {props.hasLabel && 14 | 15 | } 16 |
    17 |
    {props.value}
    18 |
    19 |
    20 | ); 21 | 22 | DetailsFormGroup.propTypes = { 23 | hasLabel: PropTypes.bool, 24 | label: PropTypes.string, 25 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), 26 | icon: PropTypes.node, 27 | name: PropTypes.string, 28 | // colWidth: PropTypes.string, 29 | }; 30 | 31 | DetailsFormGroup.defaultProps = { 32 | hasIcon: false, 33 | hasLabel: false, 34 | label: '', 35 | value: '', 36 | icon: null, 37 | name: '', 38 | // colWidth: '12', 39 | }; 40 | 41 | export default DetailsFormGroup; 42 | -------------------------------------------------------------------------------- /src/shared/DetailsIconLinkFormGroup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router'; 4 | 5 | const DetailsIconLinkFormGroup = props => ( 6 |
    7 |
    8 | { 9 | props.inWindow ? 10 | 11 | {props.icon} {props.label} 12 | 13 | : 14 | 15 | {props.icon} {props.label} 16 | 17 | } 18 |
    19 |
    20 | ); 21 | 22 | DetailsIconLinkFormGroup.propTypes = { 23 | label: PropTypes.string, 24 | icon: PropTypes.node, 25 | href: PropTypes.string, 26 | title: PropTypes.string, 27 | inWindow: PropTypes.bool, 28 | colWidth: PropTypes.string, 29 | }; 30 | 31 | DetailsIconLinkFormGroup.defaultProps = { 32 | label: '', 33 | icon: , 34 | href: 'www.ashevillenc.gov', 35 | title: 'City of Asheville Website', 36 | inWindow: false, 37 | colWidth: '12', 38 | }; 39 | 40 | export default DetailsIconLinkFormGroup; 41 | -------------------------------------------------------------------------------- /src/app/search/searchByEntities/searchByEntities.css: -------------------------------------------------------------------------------- 1 | .searchEntitiesUL { 2 | list-style: none; 3 | text-align: right; 4 | padding-left: 0px; 5 | } 6 | 7 | .searchEntitiesUL li { 8 | font-size: 0.8em; 9 | display: inline-block; 10 | text-align: center; 11 | margin-right: 5px; 12 | margin-top: 10px; 13 | cursor: pointer; 14 | } 15 | 16 | .searchEntitiesUL input[type="checkbox"] { 17 | vertical-align: top; 18 | } 19 | 20 | .searchEntitiesUL input[type="checkbox"]:focus { 21 | outline: #417c9e 3px solid; 22 | outline-offset: 1px; 23 | } 24 | 25 | .searchEntitiesUL input[type="checkbox"]:before { 26 | top: 2px; 27 | left: 5px; 28 | width: 5px; 29 | height: 11px; 30 | } 31 | 32 | .searchEntitiesUL input[type="checkbox"]:checked:after { 33 | background-color: #4579B3; 34 | border-color: #4579B3; 35 | } 36 | 37 | .searchEntitiesUL input[type="checkbox"]:after { 38 | width: 14px; 39 | height: 14px; 40 | margin-top: 2px; 41 | margin-right: 0px; 42 | border: 1px solid #888; 43 | border-radius: 1px; 44 | } 45 | 46 | .entityDescription { 47 | display: block; 48 | /*padding-left: 10px;*/ 49 | } 50 | 51 | .unchecked { 52 | color: #888; 53 | } 54 | 55 | .entityDescriptionUnchecked { 56 | display: block; 57 | padding-left: 10px; 58 | color: #888; 59 | } -------------------------------------------------------------------------------- /src/shared/Icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | // https://medium.com/@david.gilbertson/icons-as-react-components-de3e33cb8792 5 | 6 | const Icon = props => { 7 | const styles = { 8 | svg: { 9 | display: 'inline-block', 10 | verticalAlign: props.verticalAlign, 11 | }, 12 | path: { 13 | fill: props.color || 'currentColor', 14 | }, 15 | }; 16 | 17 | return ( 18 | 25 | 26 | {props.path.split(',').map((path, index) => 27 | 32 | )} 33 | 34 | 35 | ); 36 | }; 37 | 38 | Icon.propTypes = { 39 | path: PropTypes.string.isRequired, 40 | size: PropTypes.number, 41 | color: PropTypes.string, 42 | viewBox: PropTypes.string, 43 | verticalAlign: PropTypes.string, 44 | }; 45 | 46 | Icon.defaultProps = { 47 | size: 16, 48 | verticalAlign: 'middle', 49 | }; 50 | 51 | export default Icon; 52 | -------------------------------------------------------------------------------- /src/app/development/sla_dashboard/SLA_utilities.js: -------------------------------------------------------------------------------- 1 | export const getTasks = () => ( 2 | [ 3 | 'Addressing', 4 | 'Building Review', 5 | 'Fire Review', 6 | 'Zoning Review', 7 | 'Driveway', 8 | 'Grading', 9 | 'Stormwater', 10 | ] 11 | ); 12 | 13 | export const getAverageCounts = (slaData) => { 14 | const formattedData = {}; 15 | for (let task of getTasks()) { 16 | formattedData[task] = []; 17 | } 18 | for (let record of slaData) { 19 | const item = Object.assign({}, { 20 | month: record.month, 21 | year: record.year, 22 | displayDate: [record.month, record.year].join('/') 23 | }); 24 | item[[record.task, 'Met SLA'].join(' ')] = record.met_sla; 25 | item[[record.task, 'Past SLA'].join(' ')] = record.past_sla; 26 | item[[record.task, 'Met SLA Percent'].join(' ')] = Math.round(record.met_sla_percent); 27 | formattedData[record.task].push(item); 28 | } 29 | for (let task of getTasks()) { 30 | formattedData[task].sort((a, b) => { 31 | if (a.year < b.year) { 32 | return -1; 33 | } 34 | if (a.year > b.year) { 35 | return 1; 36 | } 37 | return ((a.month < b.month) ? -1 : ((a.month > b.month) ? 1 : 0)) // eslint-disable-line 38 | }); 39 | } 40 | return formattedData; 41 | }; 42 | -------------------------------------------------------------------------------- /src/shared/Select.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Select extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { value: this.selected }; 9 | this.options = props.options; 10 | this.value = props.value; 11 | this.name = props.name; 12 | this.id = props.id; 13 | this.handleChange = this.handleChange.bind(this); 14 | } 15 | 16 | handleChange(event) { 17 | this.setState({ 18 | selected: event.target.value, 19 | }); 20 | } 21 | 22 | render() { 23 | return ( 24 | 29 | ); 30 | } 31 | } 32 | 33 | const optionShape = { 34 | value: PropTypes.string, 35 | display: PropTypes.string, 36 | }; 37 | 38 | Select.propTypes = { 39 | options: PropTypes.arrayOf(PropTypes.shape(optionShape)), 40 | value: PropTypes.string, 41 | name: PropTypes.string, 42 | id: PropTypes.string, 43 | }; 44 | 45 | export default Select; 46 | 47 | -------------------------------------------------------------------------------- /src/app/budget/BudgetData.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { browserHistory } from 'react-router'; 3 | import PageHeader from '../../shared/PageHeader'; 4 | import ButtonGroup from '../../shared/ButtonGroup'; 5 | import Button from '../../shared/Button'; 6 | import Icon from '../../shared/Icon'; 7 | import { IM_COIN_DOLLAR } from '../../shared/iconConstants'; 8 | import { withLanguage } from '../../utilities/lang/LanguageContext'; 9 | import { english } from './english'; 10 | import { spanish } from './spanish'; 11 | 12 | const BudgetData = (props) => { 13 | let content; 14 | switch (props.language.language) { 15 | case 'Spanish': 16 | content = spanish; 17 | break; 18 | default: 19 | content = english; 20 | } 21 | 22 | return ( 23 |
    24 | } 27 | > 28 | 29 | 30 | 31 | 32 |
    33 |
    34 |

    {content.budget_data_explanation}

    35 |
    36 |
    37 |
    38 | ); 39 | }; 40 | 41 | export default withLanguage(BudgetData); 42 | -------------------------------------------------------------------------------- /src/app/CityInfoBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IndexLink } from 'react-router'; 3 | 4 | const CityInfoBar = () => ( 5 |
    6 |
    7 |
    8 | City of Asheville logo 9 |
    10 |
    11 |
    12 |
    Dashboards
    13 | City of Asheville, NC 14 |
    15 |
    16 |
    17 |
    18 | Click here to give feedback or sign up for user testing 19 |
    20 |
    21 | ); 22 | 23 | export default CityInfoBar; 24 | -------------------------------------------------------------------------------- /src/app/capital_projects/CIPData.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { browserHistory } from 'react-router'; 3 | import PageHeader from '../../shared/PageHeader'; 4 | import ButtonGroup from '../../shared/ButtonGroup'; 5 | import Button from '../../shared/Button'; 6 | import Icon from '../../shared/Icon'; 7 | import { IM_CITY } from '../../shared/iconConstants'; 8 | import { withLanguage } from '../../utilities/lang/LanguageContext'; 9 | import { english } from './english'; 10 | import { spanish } from './spanish'; 11 | 12 | const CIPData = (props) => { 13 | // set language 14 | let content; 15 | switch (props.language.language) { 16 | case 'Spanish': 17 | content = spanish; 18 | break; 19 | default: 20 | content = english; 21 | } 22 | 23 | return ( 24 |
    25 | } 28 | > 29 | 30 | 31 | 32 | 33 |
    34 |
    35 |

    {content.data_info}

    36 |
    37 |
    38 |
    39 | ); 40 | }; 41 | 42 | export default withLanguage(CIPData); 43 | -------------------------------------------------------------------------------- /src/app/development/trc/PermitTypeCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TypePuck from './TypePuck'; 3 | import { trcProjectTypes, descriptorTitles } from './textContent'; 4 | 5 | const PermitTypeCard = ({ type }) => { 6 | const projectType = trcProjectTypes[type]; 7 | return ( 8 |
    17 |
    18 | 23 | 24 | {type} 25 | 26 |
    27 |
    28 | {Object.keys(projectType.descriptors).map(key => ( 29 |
    30 |
    31 | {descriptorTitles[key]} 32 |
    33 | {projectType.descriptors[key]} 34 |
    35 | ))} 36 |
    37 |
    38 | ); 39 | }; 40 | 41 | export default PermitTypeCard; 42 | -------------------------------------------------------------------------------- /src/app/development/trc/ArrowDefs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { trcProjectTypes } from './textContent'; 4 | 5 | /* 6 | Make little triangles that we can use at the end of the diagram links by referencing their id attribute. 7 | 8 | From https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs: 9 | "The element is used to store graphical objects that will be used at a later time. Objects created inside a element are not rendered directly. To display them you have to reference them (with a element for example)." 10 | */ 11 | const ArrowDefs = ({ arrowWidth }) => ( 12 | 13 | {Object.values(trcProjectTypes).map(type => ( 14 | 24 | 28 | 29 | ))} 30 | 31 | ); 32 | 33 | ArrowDefs.propTypes = { 34 | arrowWidth: PropTypes.number.isRequired, 35 | }; 36 | 37 | export default ArrowDefs; 38 | -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_image.scss: -------------------------------------------------------------------------------- 1 | // Image Mixins 2 | // - Responsive image 3 | // - Retina image 4 | 5 | 6 | // Responsive image 7 | // 8 | // Keep images from scaling beyond the width of their parents. 9 | @mixin img-responsive($display: block) { 10 | display: $display; 11 | max-width: 100%; // Part 1: Set a maximum relative to the parent 12 | height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching 13 | } 14 | 15 | 16 | // Retina image 17 | // 18 | // Short retina mixin for setting background-image and -size. Note that the 19 | // spelling of `min--moz-device-pixel-ratio` is intentional. 20 | @mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) { 21 | background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-1x}"), "#{$file-1x}")); 22 | 23 | @media 24 | only screen and (-webkit-min-device-pixel-ratio: 2), 25 | only screen and ( min--moz-device-pixel-ratio: 2), 26 | only screen and ( -o-min-device-pixel-ratio: 2/1), 27 | only screen and ( min-device-pixel-ratio: 2), 28 | only screen and ( min-resolution: 192dpi), 29 | only screen and ( min-resolution: 2dppx) { 30 | background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-2x}"), "#{$file-2x}")); 31 | background-size: $width-1x $height-1x; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/development/trc/TRCDataTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import PropTypes from 'prop-types'; 3 | import { timeDay, timeMonth } from 'd3-time'; 4 | import PermitsTableWrapper from '../permits/PermitsTableWrapper'; 5 | import TimeSlider from '../volume/TimeSlider'; 6 | import ErrorBoundary from '../../../shared/ErrorBoundary'; 7 | import { trcProjectTypes } from './textContent'; 8 | 9 | 10 | class TRCDataTable extends React.Component { 11 | constructor() { 12 | super(); 13 | const now = timeDay.floor(new Date()); 14 | this.initialBrushExtent = [ 15 | timeMonth.offset(now, -2).getTime(), 16 | now.getTime(), 17 | ]; 18 | this.state = { 19 | timeSpan: this.initialBrushExtent, 20 | }; 21 | } 22 | 23 | render() { 24 | return (
    25 | 26 | this.setState({ 28 | timeSpan: newExtent, 29 | })} 30 | defaultBrushExtent={this.initialBrushExtent} 31 | xSpan={2} 32 | tickMeasure="month" 33 | /> 34 | 40 | 41 |
    ); 42 | } 43 | } 44 | 45 | export default TRCDataTable; 46 | -------------------------------------------------------------------------------- /src/styles/bootstrap/bootstrap.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2016 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/blob/master/LICENSE) 5 | */ 6 | 7 | // Core variables and mixins 8 | @import "variables"; 9 | @import "mixins"; 10 | 11 | // Reset and dependencies 12 | @import "normalize"; 13 | @import "print"; 14 | 15 | // Core CSS 16 | @import "scaffolding"; 17 | @import "type"; 18 | @import "code"; 19 | @import "grid"; 20 | @import "tables"; 21 | @import "forms"; 22 | @import "buttons"; 23 | 24 | // Components 25 | @import "component-animations"; 26 | @import "dropdowns"; 27 | @import "button-groups"; 28 | @import "input-groups"; 29 | @import "navs"; 30 | @import "navbar"; 31 | @import "breadcrumbs"; 32 | @import "pagination"; 33 | @import "pager"; 34 | @import "labels"; 35 | @import "badges"; 36 | @import "jumbotron"; 37 | @import "thumbnails"; 38 | @import "alerts"; 39 | @import "progress-bars"; 40 | @import "media"; 41 | @import "list-group"; 42 | @import "panels"; 43 | @import "responsive-embed"; 44 | @import "wells"; 45 | @import "close"; 46 | 47 | // Components w/ JavaScript 48 | @import "modals"; 49 | @import "tooltip"; 50 | @import "popovers"; 51 | @import "carousel"; 52 | 53 | // Utility classes 54 | @import "utilities"; 55 | @import "responsive-utilities"; 56 | 57 | // Theme from Bootswatch 58 | @import "bootswatch"; 59 | -------------------------------------------------------------------------------- /src/styles/bootstrap/_jumbotron.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Jumbotron 3 | // -------------------------------------------------- 4 | 5 | 6 | .jumbotron { 7 | padding-top: $jumbotron-padding; 8 | padding-bottom: $jumbotron-padding; 9 | margin-bottom: $jumbotron-padding; 10 | color: $jumbotron-color; 11 | background-color: $jumbotron-bg; 12 | 13 | h1, 14 | .h1 { 15 | color: $jumbotron-heading-color; 16 | } 17 | 18 | p { 19 | margin-bottom: ($jumbotron-padding / 2); 20 | font-size: $jumbotron-font-size; 21 | font-weight: 200; 22 | } 23 | 24 | > hr { 25 | border-top-color: darken($jumbotron-bg, 10%); 26 | } 27 | 28 | .container &, 29 | .container-fluid & { 30 | border-radius: $border-radius-large; // Only round corners at higher resolutions if contained in a container 31 | padding-left: ($grid-gutter-width / 2); 32 | padding-right: ($grid-gutter-width / 2); 33 | } 34 | 35 | .container { 36 | max-width: 100%; 37 | } 38 | 39 | @media screen and (min-width: $screen-sm-min) { 40 | padding-top: ($jumbotron-padding * 1.6); 41 | padding-bottom: ($jumbotron-padding * 1.6); 42 | 43 | .container &, 44 | .container-fluid & { 45 | padding-left: ($jumbotron-padding * 2); 46 | padding-right: ($jumbotron-padding * 2); 47 | } 48 | 49 | h1, 50 | .h1 { 51 | font-size: $jumbotron-heading-font-size; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/shared/InCityMessage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Icon from '../shared/Icon'; 4 | import { withLanguage } from '../utilities/lang/LanguageContext'; 5 | import { IM_LIBRARY2, IM_TARGET } from '../shared/iconConstants'; 6 | 7 | const spanish = { 8 | in_city: 'Est\xE1 dentro de los l\xEDmites de la ciudad', 9 | out_of_city: 'Est\xE1 fuera de los l\xEDmites de la ciudad', 10 | }; 11 | 12 | const english = { 13 | in_city: 'It\'s in the city', 14 | out_of_city: 'It\'s outside of the city', 15 | }; 16 | 17 | const translate = (value, language) => { 18 | switch (language) { 19 | case 'Spanish': 20 | return spanish[value]; 21 | default: 22 | return english[value]; 23 | } 24 | }; 25 | 26 | const InCityMessage = props => ( 27 |
    28 |
    29 | {props.icon && 30 | 31 | } 32 | {translate(props.inTheCity ? 'in_city' : 'out_of_city', props.language.language)} 33 |
    34 |
    35 | ); 36 | 37 | InCityMessage.propTypes = { 38 | inTheCity: PropTypes.bool, 39 | icon: PropTypes.bool, 40 | }; 41 | 42 | InCityMessage.defaultProps = { 43 | inTheCity: true, 44 | icon: true, 45 | }; 46 | 47 | export default withLanguage(InCityMessage); 48 | -------------------------------------------------------------------------------- /src/styles/components/projects.scss: -------------------------------------------------------------------------------- 1 | .kanban-phase { 2 | border: 3px solid #d8dada; 3 | border-radius: 4px; 4 | padding-left: 4px; 5 | padding-right: 4px; 6 | height: 400px; 7 | overflow-y: auto; 8 | } 9 | .kanban-item { 10 | background: orange; 11 | border: 2px solid orange; 12 | margin: 3px; 13 | height: 45px; 14 | border-radius: 2px; 15 | //cursor: pointer; 16 | } 17 | 18 | .kanban-col { 19 | padding-left: 5px; 20 | padding-right: 5px; 21 | } 22 | 23 | .kanban-badge { 24 | display: inline-block; 25 | min-width: 10px; 26 | padding: 3px 14px; 27 | margin-left: 5px; 28 | font-size: 25px; 29 | font-weight: bold; 30 | color: #fff; 31 | line-height: 1; 32 | vertical-align: middle; 33 | white-space: nowrap; 34 | text-align: center; 35 | background-color: orange; 36 | border-radius: 10px; 37 | } 38 | 39 | .kanban-icon { 40 | padding-top: 7px; 41 | width: 30px; 42 | display: inline; 43 | float: left; 44 | } 45 | 46 | .kanban-text { 47 | display: inline; 48 | padding-left: 4px; 49 | display: -webkit-box; 50 | margin: 0 auto; 51 | -webkit-line-clamp: 2; 52 | -webkit-box-orient: vertical; 53 | overflow: hidden; 54 | text-overflow: ellipsis; 55 | background: white; 56 | height: 100%; 57 | } 58 | 59 | .kanban-project { 60 | background-color: #4077a5; 61 | border: 2px solid #4077a5; 62 | } 63 | 64 | .kanban-badge.kanban-project { 65 | border: none; 66 | background-color: #4077a5; 67 | } -------------------------------------------------------------------------------- /src/app/budget/BudgetSankey.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Query } from 'react-apollo'; 3 | import { getSankeyData } from './graphql/budgetQueries'; 4 | import Sankey from '../../shared/visualization/Sankey'; 5 | import LoadingAnimation from '../../shared/LoadingAnimation'; 6 | import Error from '../../shared/Error'; 7 | import { withLanguage } from '../../utilities/lang/LanguageContext'; 8 | import { english } from './english'; 9 | import { spanish } from './spanish'; 10 | 11 | const BudgetSankey = props => ( 12 | 15 | {({ loading, error, data }) => { 16 | if (loading) return ; 17 | if (error) return ; 18 | 19 | // set language 20 | let content; 21 | switch (props.language.language) { 22 | case 'Spanish': 23 | content = spanish; 24 | break; 25 | default: 26 | content = english; 27 | } 28 | return ( 29 | { 34 | if (!value || value === 0) { return '$0'; } 35 | return [value < 0 ? '-$' : '$', Math.abs(value).toLocaleString()].join(''); 36 | }} 37 | /> 38 | ); 39 | }} 40 | 41 | ); 42 | 43 | export default withLanguage(BudgetSankey); 44 | -------------------------------------------------------------------------------- /docs/requirements/principles.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | # Requirements & Getting Started 5 | 6 | ## Basic requirements 7 | * SimpliCity II is written in React. You will need: 8 | * Node 9 | * Webpack 10 | * Yarn 11 | * A code editor (Visual Studio Code recommended) 12 | 13 | ## Getting Started 14 | * Fork the repo and clone locally 15 | * Navigate to the project directory on your machine 16 | * Run "yarn" command in git bash to get all the packages as described by the package.json 17 | * Run "npm start" command to run the app at localhost:3000 18 | 19 | ## Notes on contributing 20 | * Fork the repo and branch off the development branch. We use this [git branching model](http://nvie.com/posts/a-successful-git-branching-model/) 21 | * General guidlines for [contributing to open source projects](https://opensource.guide/how-to-contribute/#how-to-submit-a-contribution) 22 | 23 | ## If adapting the code base for your city 24 | * See the companion [simplicity-graphql-server repo](https://github.com/cityofasheville/simplicity-graphql-server) for the API 25 | * To run simplicity-graphql-server or your own API locally during development, you can specificy in an .env file whether or not to USE_LOCAL_API, and edit the graphql.js file to update the API URLs as needed. 26 | * If you wish for mobile users to be able to save the application to their homepage, remember to create your own manifest.json file. 27 | * Remember to update your README's and your package.json files with your information. 28 | -------------------------------------------------------------------------------- /src/styles/components/disclaimer.scss: -------------------------------------------------------------------------------- 1 | /* reset */ 2 | button { 3 | all: unset; 4 | } 5 | 6 | .AlertDialogOverlay { 7 | z-index: 9998; 8 | backdrop-filter: blur(4px); 9 | background-color: rgba(0, 0, 0, 0.3); 10 | position: fixed; 11 | inset: 0; 12 | animation: overlayShow .5s cubic-bezier(0.16, 1, 0.3, 1); 13 | } 14 | 15 | .AlertDialogContent { 16 | z-index: 9999; 17 | background-color: white; 18 | border-radius: 6px; 19 | box-shadow: 20 | hsl(206 22% 7% / 35%) 0px 10px 38px -10px, 21 | hsl(206 22% 7% / 20%) 0px 10px 20px -15px; 22 | position: fixed; 23 | top: 50%; 24 | left: 50%; 25 | transform: translate(-50%, -50%); 26 | width: 90vw; 27 | max-width: 500px; 28 | max-height: 85vh; 29 | padding: 25px; 30 | animation: contentShow .5s cubic-bezier(0.16, 1, 0.3, 1); 31 | } 32 | .AlertDialogContent:focus { 33 | outline: none; 34 | } 35 | 36 | .AlertDialogTitle { 37 | margin-top: 5px; 38 | margin-bottom: 30px; 39 | color: var(--mauve-12); 40 | font-size: 17px; 41 | font-weight: 500; 42 | } 43 | 44 | .AlertDialogDescription { 45 | margin-bottom: 20px; 46 | color: var(--mauve-11); 47 | font-size: 15px; 48 | line-height: 1.5; 49 | } 50 | 51 | @keyframes overlayShow { 52 | from { 53 | opacity: 0; 54 | } 55 | to { 56 | opacity: 1; 57 | } 58 | } 59 | 60 | @keyframes contentShow { 61 | from { 62 | opacity: 0; 63 | transform: translate(-50%, -48%) scale(0.96); 64 | } 65 | to { 66 | opacity: 1; 67 | transform: translate(-50%, -50%) scale(1); 68 | } 69 | } 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/app/search/Search.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql, compose } from 'react-apollo'; 3 | import { getSearchText } from './graphql/searchQueries'; 4 | import { updateSearchText } from './graphql/searchMutations'; 5 | 6 | import SearchBar from './SearchBar'; 7 | 8 | let timeout = null; 9 | 10 | function Search (props) { 11 | console.log('Search props', props); 12 | return ( 13 |
    14 |
    15 | { 19 | e.persist(); 20 | clearTimeout(timeout); 21 | timeout = setTimeout(() => { 22 | console.log('debounced bit!', e.target.value); 23 | props.updateSearchText({ 24 | variables: { 25 | text: e.target.value, 26 | }, 27 | }); 28 | }, 500); 29 | }} 30 | onSearchClick={text => props.updateSearchText({ 31 | variables: { 32 | text, 33 | }, 34 | })} 35 | location={props.location} 36 | /> 37 |
    38 |
    39 | ); 40 | } 41 | 42 | export default compose( 43 | graphql(updateSearchText, { name: 'updateSearchText' }), 44 | graphql(getSearchText, { 45 | props: ({ data: { searchText } }) => ({ 46 | searchText, 47 | }), 48 | }) 49 | )(Search); 50 | -------------------------------------------------------------------------------- /src/shared/visualization/MapLegendControl.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { MapControl } from 'react-leaflet'; 4 | import L from 'leaflet'; 5 | 6 | export default class MapLegendControl extends MapControl { 7 | componentWillMount() { 8 | const legend = L.control({ position: 'bottomright' }); 9 | const jsx = ( 10 |
    11 | {this.props.children} 12 |
    13 | ); 14 | 15 | legend.onAdd = function (map) { 16 | const div = L.DomUtil.create('div', ''); 17 | L.DomEvent.on(div, 'click', (e) => { 18 | if (!e.target.classList.contains('closeLegend')) { 19 | if (e.target.tagName === 'svg') { 20 | e.target.parentNode.nextSibling.style.display = e.target.parentNode.nextSibling.style.display === 'block' ? 'none' : 'block'; 21 | } else if (e.target.tagName === 'path') { 22 | e.target.parentNode.parentNode.parentNode.nextSibling.style.display = e.target.parentNode.parentNode.parentNode.nextSibling.style.display === 'block' ? 'none' : 'block'; 23 | } else { 24 | e.target.nextSibling.style.display = e.target.nextSibling.style.display === 'block' ? 'none' : 'block'; 25 | } 26 | } 27 | }); 28 | const root = createRoot(div); 29 | root.render(jsx); 30 | return div; 31 | }; 32 | 33 | this.leafletElement = legend; 34 | } 35 | } 36 | 37 | MapLegendControl.defaultProps = { 38 | id: 'legend', 39 | }; 40 | -------------------------------------------------------------------------------- /src/styles/bootstrap/_badges.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Badges 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | .badge { 8 | display: inline-block; 9 | min-width: 10px; 10 | padding: 3px 7px; 11 | font-size: $font-size-small; 12 | font-weight: $badge-font-weight; 13 | color: $badge-color; 14 | line-height: $badge-line-height; 15 | vertical-align: middle; 16 | white-space: nowrap; 17 | text-align: center; 18 | background-color: $badge-bg; 19 | border-radius: $badge-border-radius; 20 | 21 | // Empty badges collapse automatically (not available in IE8) 22 | &:empty { 23 | display: none; 24 | } 25 | 26 | // Quick fix for badges in buttons 27 | .btn & { 28 | position: relative; 29 | top: -1px; 30 | } 31 | 32 | .btn-xs &, 33 | .btn-group-xs > .btn & { 34 | top: 0; 35 | padding: 1px 5px; 36 | } 37 | 38 | // [converter] extracted a& to a.badge 39 | 40 | // Account for badges in navs 41 | .list-group-item.active > &, 42 | .nav-pills > .active > a > & { 43 | color: $badge-active-color; 44 | background-color: $badge-active-bg; 45 | } 46 | 47 | .list-group-item > & { 48 | float: right; 49 | } 50 | 51 | .list-group-item > & + & { 52 | margin-right: 5px; 53 | } 54 | 55 | .nav-pills > li > a > & { 56 | margin-left: 3px; 57 | } 58 | } 59 | 60 | // Hover state, but only for links 61 | a.badge { 62 | &:hover, 63 | &:focus { 64 | color: $badge-link-hover-color; 65 | text-decoration: none; 66 | cursor: pointer; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/styles/bootstrap/_labels.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Labels 3 | // -------------------------------------------------- 4 | 5 | .label { 6 | display: inline; 7 | padding: .2em .6em .3em; 8 | font-size: 75%; 9 | font-weight: bold; 10 | line-height: 1; 11 | color: $label-color; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | border-radius: .25em; 16 | 17 | // [converter] extracted a& to a.label 18 | 19 | // Empty labels collapse automatically (not available in IE8) 20 | &:empty { 21 | display: none; 22 | } 23 | 24 | // Quick fix for labels in buttons 25 | .btn & { 26 | position: relative; 27 | top: -1px; 28 | } 29 | } 30 | 31 | .radioGroup div > label { 32 | font-weight: normal; 33 | cursor: pointer; 34 | } 35 | 36 | // Add hover effects, but only for links 37 | a.label { 38 | &:hover, 39 | &:focus { 40 | color: $label-link-hover-color; 41 | text-decoration: none; 42 | cursor: pointer; 43 | } 44 | } 45 | 46 | // Colors 47 | // Contextual variations (linked labels get darker on :hover) 48 | 49 | .label-default { 50 | @include label-variant($label-default-bg); 51 | } 52 | 53 | .label-primary { 54 | @include label-variant($label-primary-bg); 55 | } 56 | 57 | .label-success { 58 | @include label-variant($label-success-bg); 59 | } 60 | 61 | .label-info { 62 | @include label-variant($label-info-bg); 63 | } 64 | 65 | .label-warning { 66 | @include label-variant($label-warning-bg); 67 | } 68 | 69 | .label-danger { 70 | @include label-variant($label-danger-bg); 71 | } 72 | -------------------------------------------------------------------------------- /src/styles/components/Accordion.scss: -------------------------------------------------------------------------------- 1 | .panel { 2 | border: 1px solid #fff; 3 | color: inherit; 4 | } 5 | 6 | .panel a { 7 | color: inherit; 8 | } 9 | 10 | .panel-heading { 11 | background-color: #f2f2f2; 12 | border-bottom: 1px solid gray; 13 | } 14 | 15 | .accordion-panel-body { 16 | padding: 1rem; 17 | } 18 | 19 | .accordion-panel-body a { 20 | text-decoration: underline; 21 | } 22 | 23 | .accordion-panel-body a:focus, .accordion-panel-body a:hover { 24 | color: #4077a5; 25 | } 26 | 27 | .panel-collapse { 28 | border: 1px solid #f2f2f2; 29 | } 30 | 31 | .panel-title { 32 | padding: 0 1rem 0 0; 33 | } 34 | 35 | .panel-title-after { 36 | position: relative; 37 | height: 0; 38 | width: 0; 39 | display: inline; 40 | float: right; 41 | } 42 | 43 | .panel-title-after:after { 44 | content: ''; 45 | width: 0; 46 | height: 0; 47 | border-left: 0.4em solid transparent; 48 | border-right: 0.4em solid transparent; 49 | border-top: 0.4em solid gray; 50 | background-color: none; 51 | text-align: right; 52 | 53 | /*Adjust for position however you want*/ 54 | right: 0.75em; 55 | top: 0.5em; 56 | position: absolute; 57 | pointer-events: none; 58 | margin-right: -2rem; 59 | } 60 | 61 | .panel-title-after.open:after { 62 | transform: rotate(180deg); 63 | } 64 | 65 | // For nested accordions 66 | .accordion-root .accordion-root { 67 | margin: 0 0.5rem; 68 | 69 | .accordion-item-set { 70 | border-bottom: 1px solid #f2f2f2; 71 | } 72 | 73 | .panel-heading { 74 | background-color: transparent; 75 | } 76 | 77 | .panel-collapse { 78 | border: none; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/Topics.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import TopicCard from '../shared/TopicCard'; 4 | import { withLanguage } from '../utilities/lang/LanguageContext'; 5 | import { 6 | IM_TREE 7 | } from '../shared/iconConstants'; 8 | import Icon from '../shared/Icon'; 9 | 10 | const Topics = props => ( 11 |
    12 |
    13 |
    14 |

    View citywide topic dashboards about your community.

    15 |
    16 |
    17 |
    18 | {props.topics.map((topic, i) => ( 19 |
    20 | 21 |
    22 | ))} 23 |
    24 |
    25 | ); 26 | 27 | // Topics.propTypes = { 28 | // topics: PropTypes.arrayOf([PropTypes.oneOf(PropTypes.string, PropTypes.shape({}))]), 29 | // }; 30 | 31 | Topics.defaultProps = { 32 | topics: [ 33 | // { 34 | // name: 'BUDGET', 35 | // path: 'budget', 36 | // }, 37 | { 38 | name: 'CAPITAL_PROJECTS', 39 | path: 'capital_projects', 40 | }, 41 | { 42 | name: 'DEVELOPMENT_DASHBOARD', 43 | path: '/development/major' 44 | }, 45 | { 46 | name: 'CLIMATE', 47 | path: 'https://avl.maps.arcgis.com/apps/instant/lookup/index.html?appid=10e2c4ae45614b92ad4efaa61342b249%2F' 48 | }, 49 | ], 50 | }; 51 | 52 | export default withLanguage(Topics); 53 | -------------------------------------------------------------------------------- /src/utilities/generalUtilities.js: -------------------------------------------------------------------------------- 1 | import { browserHistory } from 'react-router'; 2 | 3 | export const timeOptions = [ 4 | { display: 'the last 30 days', value: '30' }, 5 | { display: 'the last 6 months', value: '183' }, 6 | { display: 'the last year', value: '365' }, 7 | { display: 'the last 2 years', value: '730' }, 8 | { display: 'the last 5 years', value: '1825' }, 9 | { display: 'the last 10 years', value: '3650' }, 10 | { display: 'all time', value: 'all' }, 11 | ]; 12 | 13 | export const extentOptions = [ 14 | // { display: 'a quarter block (27.5 yards)', value: '83' }, 15 | // { display: 'half a block (55 yards)', value: '165' }, 16 | { display: 'a city block (110 yards)', value: '330' }, 17 | { display: 'a couple city blocks (1/8 mile)', value: '660' }, 18 | { display: 'a quarter mile', value: '1320' }, 19 | { display: 'a half mile', value: '2640' }, 20 | { display: 'a mile', value: '5280' }, 21 | ]; 22 | 23 | export const refreshLocation = (updateKeyValues, location) => { 24 | let urlStr = location.pathname; 25 | const urlParams = Array.from(new Set(Object.keys(updateKeyValues).concat(Object.keys(location.query)))); 26 | const paramsToUpdate = Object.keys(updateKeyValues); 27 | if (urlParams.length > 0) { 28 | urlStr = `${urlStr}?`; 29 | for (let i = 0; i < urlParams.length; i += 1) { 30 | if (paramsToUpdate.indexOf(urlParams[i]) > -1) { 31 | urlStr = `${urlStr}${i === 0 ? '' : '&'}${urlParams[i]}=${updateKeyValues[urlParams[i]]}`; 32 | } else { 33 | urlStr = `${urlStr}&${urlParams[i]}=${location.query[urlParams[i]]}`; 34 | } 35 | } 36 | } 37 | browserHistory.replace(urlStr); 38 | }; 39 | -------------------------------------------------------------------------------- /src/utilities/auth/authProviderModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { graphql, compose } from 'react-apollo'; 4 | import { getModalOpen } from './graphql/authQueries'; 5 | import { updateAuthModal } from './graphql/authMutations'; 6 | 7 | const AuthProviderModal = (props) => { 8 | const display = (props.open) ? { display: 'block' } : { display: 'none' }; 9 | 10 | return ( 11 |
    12 |
    13 |
    14 |
    15 |
    16 | 28 |
    29 |
    30 |
    31 |
    32 |
    33 |
    34 |
    35 |
    36 | ); 37 | }; 38 | 39 | AuthProviderModal.propTypes = { 40 | open: PropTypes.bool, 41 | }; 42 | 43 | export default compose( 44 | graphql(updateAuthModal, { name: 'updateAuthModal' }), 45 | graphql(getModalOpen, { 46 | props: ({ data: { modal } }) => ({ 47 | open: modal.open, 48 | }), 49 | }) 50 | )(AuthProviderModal); 51 | -------------------------------------------------------------------------------- /src/shared/EmailDownload.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { CSVLink } from 'react-csv'; 4 | import Icon from './Icon'; 5 | import { IM_DOWNLOAD7 } from './iconConstants'; 6 | import ButtonGroup from './ButtonGroup'; 7 | import Button from './Button'; 8 | import { withLanguage } from '../utilities/lang/LanguageContext'; 9 | 10 | const spanish = { 11 | Email: 'Mandar correo electr\xF3', 12 | Download: 'Descargar', 13 | }; 14 | 15 | const english = { 16 | Email: 'Email', 17 | Download: 'Download', 18 | }; 19 | 20 | const translate = (value, language) => { 21 | switch (language) { 22 | case 'Spanish': 23 | return spanish[value]; 24 | case 'English': 25 | return english[value]; 26 | default: 27 | return value; 28 | } 29 | }; 30 | 31 | const EmailDownload = props => ( 32 |
    33 | 34 | 35 | 36 | {/* 37 | 38 | */} 39 |
    40 | ); 41 | 42 | EmailDownload.propTypes = { 43 | emailFunction: PropTypes.func, 44 | downloadData: PropTypes.array, 45 | fileName: PropTypes.string, 46 | lang: PropTypes.string, 47 | }; 48 | 49 | EmailDownload.defaultProps = { 50 | emailFunction: null, 51 | downloadData: [], 52 | lang: 'English', 53 | }; 54 | 55 | export default withLanguage(EmailDownload); 56 | -------------------------------------------------------------------------------- /src/shared/LoadingAnimation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const LoadingAnimation = (props) => { 5 | switch (props.size) { 6 | case 'small': 7 | return ( 8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    {props.message}
    17 |
    18 | ); 19 | default: 20 | return ( 21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    {props.message}
    30 |
    31 | ); 32 | } 33 | }; 34 | 35 | LoadingAnimation.propTypes = { 36 | size: PropTypes.string, 37 | message: PropTypes.string, 38 | marginTop: PropTypes.string, 39 | }; 40 | 41 | LoadingAnimation.defaultProps = { 42 | name: 'large', 43 | message: 'Loading...', 44 | marginTop: '15px', 45 | }; 46 | 47 | export default LoadingAnimation; 48 | -------------------------------------------------------------------------------- /src/styles/bootstrap/_code.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Code (inline and block) 3 | // -------------------------------------------------- 4 | 5 | 6 | // Inline and block code styles 7 | code, 8 | kbd, 9 | pre, 10 | samp { 11 | font-family: $font-family-monospace; 12 | } 13 | 14 | // Inline code 15 | code { 16 | padding: 2px 4px; 17 | font-size: 90%; 18 | color: $code-color; 19 | background-color: $code-bg; 20 | border-radius: $border-radius-base; 21 | } 22 | 23 | // User input typically entered via keyboard 24 | kbd { 25 | padding: 2px 4px; 26 | font-size: 90%; 27 | color: $kbd-color; 28 | background-color: $kbd-bg; 29 | border-radius: $border-radius-small; 30 | box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); 31 | 32 | kbd { 33 | padding: 0; 34 | font-size: 100%; 35 | font-weight: bold; 36 | box-shadow: none; 37 | } 38 | } 39 | 40 | // Blocks of code 41 | pre { 42 | display: block; 43 | padding: (($line-height-computed - 1) / 2); 44 | margin: 0 0 ($line-height-computed / 2); 45 | font-size: ($font-size-base - 1); // 14px to 13px 46 | line-height: $line-height-base; 47 | word-break: break-all; 48 | word-wrap: break-word; 49 | color: $pre-color; 50 | background-color: $pre-bg; 51 | border: 1px solid $pre-border-color; 52 | border-radius: $border-radius-base; 53 | 54 | // Account for some code outputs that place code tags in pre tags 55 | code { 56 | padding: 0; 57 | font-size: inherit; 58 | color: inherit; 59 | white-space: pre-wrap; 60 | background-color: transparent; 61 | border-radius: 0; 62 | } 63 | } 64 | 65 | // Enable scrollable blocks of code 66 | .pre-scrollable { 67 | max-height: $pre-scrollable-max-height; 68 | overflow-y: scroll; 69 | } 70 | -------------------------------------------------------------------------------- /src/styles/components/DataModal.scss: -------------------------------------------------------------------------------- 1 | // See https://assortment.io/posts/accessible-modal-component-react-portals-part-1 2 | 3 | .c-modal-cover { 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | width: 100%; 8 | height: 100%; 9 | z-index: 10; // This must be at a higher index to the rest of your page content 10 | transform: translateZ(0); 11 | background-color: rgba(#000, 0.15); 12 | } 13 | 14 | .c-modal { 15 | position: fixed; 16 | top: 0; 17 | left: 0; 18 | width: 100%; 19 | height: 100%; 20 | padding: 2.5em 1.5em 1.5em 1.5em; 21 | background-color: #FFFFFF; 22 | box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.1); 23 | overflow-y: auto; 24 | -webkit-overflow-scrolling: touch; 25 | 26 | @media screen and (min-width: 500px) { 27 | left: 50%; 28 | top: 50%; 29 | height: auto; 30 | transform: translate(-50%, -50%); 31 | max-width: 30em; 32 | max-height: calc(100% - 1em); 33 | } 34 | } 35 | .c-modal__close { 36 | position: absolute; 37 | top: 0; 38 | right: 0; 39 | padding: .5em; 40 | line-height: 1; 41 | background: #f6f6f7; 42 | border: 0; 43 | box-shadow: 0; 44 | cursor: pointer; 45 | } 46 | 47 | .c-modal__close-icon { 48 | width: 25px; 49 | height: 25px; 50 | fill: transparent; 51 | stroke: black; 52 | stroke-linecap: round; 53 | stroke-width: 2; 54 | } 55 | 56 | .c-modal__body { 57 | padding-top: .25em; 58 | } 59 | 60 | .u-hide-visually { 61 | border: 0 !important; 62 | clip: rect(0 0 0 0) !important; 63 | height: 1px !important; 64 | margin: -1px !important; 65 | overflow: hidden !important; 66 | padding: 0 !important; 67 | position: absolute !important; 68 | width: 1px !important; 69 | white-space: nowrap !important; 70 | } -------------------------------------------------------------------------------- /src/styles/bootstrap/mixins/_buttons.scss: -------------------------------------------------------------------------------- 1 | // Button variants 2 | // 3 | // Easily pump out default styles, as well as :hover, :focus, :active, 4 | // and disabled options for all buttons 5 | 6 | @mixin button-variant($color, $background, $border) { 7 | color: $color; 8 | background-color: $background; 9 | border-color: $border; 10 | 11 | &:focus, 12 | &.focus { 13 | color: $color; 14 | background-color: darken($background, 20%); 15 | border-color: darken($border, 32%); 16 | } 17 | &:hover { 18 | color: $color; 19 | background-color: darken($background, 12%); 20 | border-color: darken($border, 17%); 21 | } 22 | &:active, 23 | &.active, 24 | .open > &.dropdown-toggle { 25 | color: $color; 26 | background-color: darken($background, 20%); 27 | border-color: darken($border, 32%); 28 | 29 | &:hover, 30 | &:focus, 31 | &.focus { 32 | color: $color; 33 | background-color: darken($background, 35%); 34 | border-color: darken($border, 42%); 35 | } 36 | } 37 | &:active, 38 | &.active, 39 | .open > &.dropdown-toggle { 40 | background-image: none; 41 | } 42 | &.disabled, 43 | &[disabled], 44 | fieldset[disabled] & { 45 | &:hover, 46 | &:focus, 47 | &.focus { 48 | background-color: $background; 49 | border-color: $border; 50 | } 51 | } 52 | 53 | .badge { 54 | color: $background; 55 | background-color: $color; 56 | } 57 | } 58 | 59 | // Button sizes 60 | @mixin button-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) { 61 | padding: $padding-vertical $padding-horizontal; 62 | font-size: $font-size; 63 | line-height: $line-height; 64 | border-radius: $border-radius; 65 | } 66 | -------------------------------------------------------------------------------- /src/shared/visualization/HorizontalLegend.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { labelOrder } from './visUtilities'; 4 | 5 | 6 | const HorizontalLegend = (props) => { 7 | const rectWidth = 15; 8 | 9 | const labelItems = props.labelItems || labelOrder(props.formattedData, props.valueAccessor); 10 | 11 | return (
    15 | {labelItems.map((item, index) => { 16 | const label = props.legendLabelFormatter(item.label); 17 | return (
    26 | 33 | 42 | 43 | 44 | {label} 45 | 46 |
    ); 47 | })} 48 |
    ); 49 | }; 50 | 51 | HorizontalLegend.propTypes = { 52 | formattedData: PropTypes.arrayOf(PropTypes.object), 53 | legendLabelFormatter: PropTypes.func, 54 | valueAccessor: PropTypes.string, 55 | }; 56 | 57 | HorizontalLegend.defaultProps = { 58 | formattedData: [], 59 | legendLabelFormatter: d => d, 60 | valueAccessor: 'value', 61 | }; 62 | 63 | export default HorizontalLegend; 64 | -------------------------------------------------------------------------------- /src/app/EnvBanner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const EnvBanner = props => { 4 | 5 | let bannerContent = ''; 6 | let bannerOverride = false; 7 | let defaultLeftMargin = 0; 8 | 9 | if (typeof process.env.REACT_APP_SUPPRESS_ENV_WARNING !== "undefined" && +process.env.REACT_APP_SUPPRESS_ENV_WARNING) { 10 | bannerOverride = true; 11 | } 12 | 13 | if ((window.location.href.indexOf('simplicity.ashevillenc.gov') === -1 14 | && window.location.href.indexOf('http://localhost:3000/') === -1 15 | && window.location.href.indexOf('climatej.d1thp43hcib1lz.amplifyapp.com') === -1) 16 | && !bannerOverride) { 17 | 18 | let productionPathAddons = ''; 19 | 20 | if (window.location.href.indexOf('development/major') > -1) { 21 | defaultLeftMargin = 200; 22 | } 23 | 24 | if (window.location.pathname.length) { 25 | productionPathAddons += window.location.pathname; 26 | } 27 | 28 | if (window.location.search.length) { 29 | productionPathAddons += window.location.search; 30 | } 31 | 32 | bannerContent = ( 33 |
    34 |

    Attention!

    This is an experimental version of SimpliCity, and may 35 | produce unexpected results. Unless you understand how you've ended up here, we 36 | recommend visiting this page in the production version of SimpliCity instead.

    37 |
    38 | ); 39 | } 40 | 41 | return ( 42 | bannerContent 43 | ); 44 | } 45 | 46 | export default EnvBanner; 47 | -------------------------------------------------------------------------------- /src/app/development/volume/PermitDataQuery.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Query } from 'react-apollo'; 4 | import { GET_PERMITS } from './granularUtils'; 5 | import LoadingAnimation from '../../../shared/LoadingAnimation'; 6 | import VolumeDataReceivers from './VolumeDataReceivers'; 7 | 8 | function capitalizeFirstLetter(string) { 9 | return string.charAt(0).toUpperCase() + string.slice(1); 10 | } 11 | 12 | const PermitDataQuery = (props) => { 13 | const permitGroupParam = props.location.query && props.location.query.permit_group 14 | let permitGroups = ['Permits', 'Planning', 'Services']; 15 | if (permitGroupParam) { 16 | permitGroups = props.location.query.permit_group.split(',').map(m => capitalizeFirstLetter(m)); 17 | } 18 | 19 | return ( 28 | {({ loading, error, data }) => { 29 | if (loading) return ; 30 | if (error) { 31 | console.log(error); 32 | return
    Error :(
    ; 33 | } 34 | return (
    35 | {permitGroupParam && permitGroups.length === 1 &&

    Module: {permitGroups[0]}

    } 36 | {permitGroupParam && permitGroups.length > 1 &&

    Modules: {permitGroups.join(', ')}

    } 37 |
    38 | 43 |
    44 |
    ); 45 | }} 46 |
    ); 47 | }; 48 | 49 | export default PermitDataQuery; 50 | -------------------------------------------------------------------------------- /src/styles/components/multiSelect.scss: -------------------------------------------------------------------------------- 1 | .react-select-menu { 2 | background-color: #fff; 3 | border: 1px solid rgba(0,0,255,0.38); 4 | padding-left: 0px; 5 | div[role="option"] { 6 | padding-left: 18px; 7 | } 8 | div[role="option"]:hover { 9 | color: #fff; 10 | background-color: #2196F3; 11 | } 12 | .isSelected { 13 | color: red; 14 | } 15 | } 16 | 17 | .multiSelect { 18 | button { 19 | width: 100%; 20 | background: transparent; 21 | border: none; 22 | height: auto; 23 | min-height: 42px; 24 | text-align: left; 25 | padding-left: 18px; 26 | } 27 | button:focus { 28 | border: 1px solid #232436; 29 | border-radius: 2px; 30 | } 31 | button:after { 32 | content: ''; 33 | float: right; 34 | margin-top: 16px; 35 | margin-right: -32px; 36 | border-top: 6px solid #7b8a8b; 37 | border-left: 3px solid transparent; 38 | border-right: 3px solid transparent; 39 | } 40 | &.form-control { 41 | padding: 0; 42 | height: auto; 43 | margin-bottom: 10px; 44 | } 45 | // &.form-control:last-of-type { 46 | // margin-bottom: 0px; 47 | // } 48 | &.form-control:focus { 49 | border: none; 50 | } 51 | } 52 | 53 | .react-select-option__checkbox { 54 | display: inline-block; 55 | } 56 | 57 | .react-select-option__label { 58 | display: inline-block; 59 | } 60 | 61 | .selectedMultiOption { 62 | display: inline-block; 63 | background-color: #E3F2FD; 64 | border: 1px solid #c2d7e7; 65 | border-radius: 2px; 66 | margin-right: 8px; 67 | margin-top: 6px; 68 | margin-bottom: 6px; 69 | padding-left: 5px; 70 | padding-right: 5px; 71 | padding-top: 2px; 72 | padding-bottom: 2px; 73 | } 74 | 75 | .multiSelectRefresh { 76 | float: right; 77 | margin-right:20px; 78 | margin-top:10px; 79 | } -------------------------------------------------------------------------------- /.eslintrc-initial-version.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true 8 | }, 9 | "plugins": [ 10 | "react", 11 | "jsx-a11y", 12 | "import" 13 | ], 14 | "rules": { 15 | "arrow-body-style": [ 16 | 2, 17 | "as-needed" 18 | ], 19 | "comma-dangle": [ 20 | 2, 21 | "always-multiline" 22 | ], 23 | "import/imports-first": 0, 24 | "import/newline-after-import": 0, 25 | "import/no-extraneous-dependencies": 2, 26 | "import/no-named-as-default": 0, 27 | "import/no-unresolved": 2, 28 | "import/prefer-default-export": 0, 29 | "indent": [ 30 | 2, 31 | 2, 32 | { 33 | "SwitchCase": 1 34 | } 35 | ], 36 | "jsx-a11y/aria-props": 2, 37 | "jsx-a11y/heading-has-content": 0, 38 | "jsx-a11y/anchor-is-valid": 2, 39 | "jsx-a11y/label-has-for": 2, 40 | "jsx-a11y/mouse-events-have-key-events": 2, 41 | "jsx-a11y/role-has-required-aria-props": 2, 42 | "jsx-a11y/role-supports-aria-props": 2, 43 | "max-len": ["error", { "code": 100, "ignoreComments": true, "ignoreUrls": true, "ignoreStrings": true, "ignoreTemplateLiterals": true}], 44 | "newline-per-chained-call": 0, 45 | "no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"], 46 | "no-use-before-define": 0, 47 | "prefer-template": 2, 48 | "quotes": [2, "single"], 49 | "react/jsx-filename-extension": 0, 50 | "react/jsx-no-target-blank": 0, 51 | "react/require-extension": 0, 52 | "react/self-closing-comp": 0, 53 | "react/sort-comp": [1, { 54 | "order": [ 55 | "everything-else", 56 | "render" 57 | ] 58 | }], 59 | "require-yield": 0 60 | } 61 | } -------------------------------------------------------------------------------- /src/styles/bootstrap/_grid.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Grid system 3 | // -------------------------------------------------- 4 | 5 | 6 | // Container widths 7 | // 8 | // Set the container width, and override it for fixed navbars in media queries. 9 | 10 | .container { 11 | @include container-fixed; 12 | 13 | @media (min-width: $screen-sm-min) { 14 | width: $container-sm; 15 | } 16 | @media (min-width: $screen-md-min) { 17 | width: $container-md; 18 | } 19 | @media (min-width: $screen-lg-min) { 20 | width: $container-lg; 21 | } 22 | } 23 | 24 | 25 | // Fluid container 26 | // 27 | // Utilizes the mixin meant for fixed width containers, but without any defined 28 | // width for fluid, full width layouts. 29 | 30 | .container-fluid { 31 | @include container-fixed; 32 | } 33 | 34 | // Row 35 | // 36 | // Rows contain and clear the floats of your columns. 37 | 38 | .row { 39 | @include make-row; 40 | } 41 | 42 | 43 | // Columns 44 | // 45 | // Common styles for small and large grid columns 46 | 47 | @include make-grid-columns; 48 | 49 | 50 | // Extra small grid 51 | // 52 | // Columns, offsets, pushes, and pulls for extra small devices like 53 | // smartphones. 54 | 55 | @include make-grid(xs); 56 | 57 | 58 | // Small grid 59 | // 60 | // Columns, offsets, pushes, and pulls for the small device range, from phones 61 | // to tablets. 62 | 63 | @media (min-width: $screen-sm-min) { 64 | @include make-grid(sm); 65 | } 66 | 67 | 68 | // Medium grid 69 | // 70 | // Columns, offsets, pushes, and pulls for the desktop device range. 71 | 72 | @media (min-width: $screen-md-min) { 73 | @include make-grid(md); 74 | } 75 | 76 | 77 | // Large grid 78 | // 79 | // Columns, offsets, pushes, and pulls for the large desktop device range. 80 | 81 | @media (min-width: $screen-lg-min) { 82 | @include make-grid(lg); 83 | } 84 | -------------------------------------------------------------------------------- /src/app/spatial_event_topic_summary/SpatialEventTopicLocationInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | // import Icon from '../../shared/Icon'; 4 | // import { IM_SEARCH } from '../../shared/iconConstants'; 5 | import { english } from './english'; 6 | import { spanish } from './spanish'; 7 | import { withLanguage } from '../../utilities/lang/LanguageContext'; 8 | 9 | const SpatialEventTopicLocationInfo = (props) => { 10 | // set language 11 | let content; 12 | switch (props.language.language) { 13 | case 'Spanish': 14 | content = spanish; 15 | break; 16 | default: 17 | content = english; 18 | } 19 | 20 | let label; 21 | switch (props.spatialType) { 22 | case 'address': 23 | label = content.of; 24 | break; 25 | case 'neighborhood': 26 | label = content.in; 27 | break; 28 | case 'street': 29 | label = content.along; 30 | break; 31 | default: 32 | label = content.in; 33 | } 34 | 35 | return ( 36 |
    37 | 38 |
    39 |
    {props.spatialDescription} 40 | {/* Change location*/} 41 |
    42 |
    43 |
    44 | ); 45 | } 46 | 47 | SpatialEventTopicLocationInfo.propTypes = { 48 | spatialType: PropTypes.string.isRequired, 49 | spatialDescription: PropTypes.string.isRequired, 50 | columnClasses: PropTypes.string 51 | }; 52 | 53 | SpatialEventTopicLocationInfo.defaultProps = { 54 | columnClasses: 'col-md-4 col-xs-12' 55 | }; 56 | 57 | export default withLanguage(SpatialEventTopicLocationInfo); 58 | -------------------------------------------------------------------------------- /src/app/climate/ClickableTile.js: -------------------------------------------------------------------------------- 1 | import { link } from "d3-shape"; 2 | import React from "react"; 3 | 4 | function ClickableTile({ image, url, text }) { 5 | const tileRef = React.useRef(); 6 | const linkRef = React.useRef(); 7 | 8 | function handleClick(e) { 9 | if (e.button === 0) { 10 | if (!e.target.matches(".card-link-action")) { 11 | linkRef.current.click(); 12 | } 13 | } 14 | } 15 | 16 | return ( 17 |
    23 | 67 |
    68 | ); 69 | } 70 | 71 | export default ClickableTile; 72 | -------------------------------------------------------------------------------- /src/utilities/counterSet.js: -------------------------------------------------------------------------------- 1 | export default class CounterSet { 2 | 3 | constructor(counters) { 4 | this.counters = {}; 5 | if (counters) { 6 | counters.forEach((counter) => { 7 | if (typeof counter === 'string') { 8 | this.counters[counter] = { type: 'simple', total: 0 }; 9 | } else { 10 | this.counters[counter.name] = { type: counter.type, total: 0, count: [], stats: [0, 0, 0] }; 11 | } 12 | }); 13 | } 14 | } 15 | 16 | createCounter(name, type = 'simple') { 17 | if (type === 'simple') { 18 | if (!(name in this.counters)) this.counters[name] = { type: 'simple', total: 0 }; 19 | } else if (!(name in this.counters)) { 20 | this.counters[name] = { type: 'full', total: 0, count: [], stats: [0, 0, 0] }; 21 | } 22 | } 23 | 24 | getValue(name) { 25 | if (name in this.counters) { 26 | return this.counters[name].total; 27 | } 28 | return null; 29 | } 30 | 31 | getStats(name) { 32 | if (name in this.counters && this.counters[name].type === 'full') { 33 | return this.counters[name].stats; 34 | } 35 | return null; 36 | } 37 | 38 | incrementCounter(name, inc = 1) { 39 | if (!(name in this.counters)) this.createCounter(name); 40 | this.counters[name].total += inc; 41 | if (this.counters[name].type === 'full') { 42 | this.counters[name].count.push(inc); 43 | } 44 | } 45 | 46 | finalizeCounter(name) { 47 | if ('count' in this.counters[name] && this.counters[name].count.length > 0) { 48 | this.counters[name].count.sort((val1, val2) => (Number(val1) - Number(val2))); 49 | this.counters[name].stats = [ 50 | this.counters[name].count[Math.floor(this.counters[name].count.length / 2)], 51 | this.counters[name].count[0], 52 | this.counters[name].count[this.counters[name].count.length - 1], 53 | ]; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/shared/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const getButtonClass = (size, type, active) => { 5 | const typeStr = [' btn', type].join('-'); 6 | switch (size) { 7 | case 'sm': 8 | return ['btn', ' btn-sm', typeStr, active ? ' active' : ''].join(''); 9 | case 'xs': 10 | return ['btn', ' btn-xs', typeStr, active ? ' active' : ''].join(''); 11 | default: 12 | return ['btn', typeStr, active ? ' active' : ''].join(''); 13 | } 14 | }; 15 | 16 | const getButtonStyle = (positionInGroup, extraStyle) => { 17 | switch (positionInGroup) { 18 | case 'left': 19 | return { borderTopRightRadius: '0px', borderBottomRightRadius: '0px', ...extraStyle }; 20 | case 'right': 21 | return { borderTopLeftRadius: '0px', borderBottomLeftRadius: '0px', ...extraStyle }; 22 | case 'middle': 23 | return { borderTopRightRadius: '0px', borderBottomRightRadius: '0px', borderTopLeftRadius: '0px', borderBottomLeftRadius: '0px', ...extraStyle }; 24 | default: 25 | return extraStyle; 26 | } 27 | }; 28 | 29 | const Button = props => ( 30 | 37 | ); 38 | 39 | Button.propTypes = { 40 | size: PropTypes.string, 41 | type: PropTypes.string, 42 | style: PropTypes.object, 43 | children: PropTypes.node, 44 | onClick: PropTypes.func, 45 | active: PropTypes.bool, 46 | positionInGroup: PropTypes.string, 47 | }; 48 | 49 | Button.defaultProps = { 50 | size: 'regular', 51 | type: 'primary', 52 | active: false, 53 | disabled: false, 54 | positionInGroup: null, // left, middle, right 55 | onClick: null, 56 | style: {}, 57 | children: undefined, 58 | }; 59 | 60 | export default Button; 61 | -------------------------------------------------------------------------------- /src/styles/bootstrap/_alerts.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Alerts 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // ------------------------- 8 | 9 | .alert { 10 | padding: $alert-padding; 11 | margin-bottom: $line-height-computed; 12 | border: 1px solid transparent; 13 | border-radius: $alert-border-radius; 14 | 15 | // Headings for larger alerts 16 | h4 { 17 | margin-top: 0; 18 | // Specified for the h4 to prevent conflicts of changing $headings-color 19 | color: inherit; 20 | } 21 | 22 | // Provide class for links that match alerts 23 | .alert-link { 24 | font-weight: $alert-link-font-weight; 25 | } 26 | 27 | // Improve alignment and spacing of inner content 28 | > p, 29 | > ul { 30 | margin-bottom: 0; 31 | } 32 | 33 | > p + p { 34 | margin-top: 5px; 35 | } 36 | } 37 | 38 | .alert-sm { 39 | padding: $alert-padding * 0.6; 40 | } 41 | 42 | // Dismissible alerts 43 | // 44 | // Expand the right padding and account for the close button's positioning. 45 | 46 | .alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. 47 | .alert-dismissible { 48 | padding-right: ($alert-padding + 20); 49 | 50 | // Adjust close link position 51 | .close { 52 | position: relative; 53 | top: -2px; 54 | right: -21px; 55 | color: inherit; 56 | } 57 | } 58 | 59 | // Alternate styles 60 | // 61 | // Generate contextual modifier classes for colorizing the alert. 62 | 63 | .alert-success { 64 | @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text); 65 | } 66 | 67 | .alert-info { 68 | @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text); 69 | } 70 | 71 | .alert-warning { 72 | @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text); 73 | } 74 | 75 | .alert-danger { 76 | @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text); 77 | } 78 | -------------------------------------------------------------------------------- /src/app/address/english.js: -------------------------------------------------------------------------------- 1 | export const english = { 2 | address: 'Address', 3 | addresses_by_street_filename: 'addresses_by_street.csv', 4 | addresses_by_neighborhood_filename: 'addresses_by_neighborhood.csv', 5 | address_and_owner_mailing_lists: 'Address & Owner Mailing Lists', 6 | about_this_address: 'About this address', 7 | back_to_neighborhood: 'Back to neighborhood', 8 | back_to_place_matches: 'Back to place matches', 9 | back_to_street: 'Back to street', 10 | back_to_search: 'Back to search', 11 | brush_collection: 'Brush Collection', 12 | brush_week: 'Brush Week', 13 | data_type: 'Address', 14 | every: 'Every', 15 | historic_district: 'Historic district', 16 | list_view: 'List view', 17 | local_landmark: 'Local landmark', 18 | map_view: 'Map view', 19 | neighborhood: 'Neighborhood', 20 | no_neighborhood_name: 'No neighborhood name', 21 | climate: ' Climate Threats and Vulnerability', 22 | no_climate: 'Unknown Census Block Group', 23 | no_city_pickup: 'No city pickup', 24 | no_information_available: 'No information available', 25 | no_results_found: 'No results found', 26 | of_next_week: 'of next week', 27 | of_this_week: 'of this week', 28 | owner: 'Owner', 29 | placeholder: 'Search...', 30 | place_on_curb_by_7am_monday: 'Place on curb by 7am Monday', 31 | property_information: 'Property information', 32 | recycle_week: 'Recycle Week', 33 | recycling_collection: 'Recycling collection', 34 | report_with_the_asheville_app: 'Report with the Asheville App', 35 | sometime_this_week: 'Sometime this week', 36 | sometime_next_week: 'Sometime next week', 37 | street: 'Street', 38 | street_maintenance: 'Street Maintenance', 39 | trash_collection: 'Trash Collection', 40 | weekdays: { 41 | MONDAY: 'MONDAY', 42 | TUESDAY: 'TUESDAY', 43 | WEDNESDAY: 'WEDNESDAY', 44 | THURSDAY: 'THURSDAY', 45 | FRIDAY: 'FRIDAY', 46 | SATURDAY: 'SATURDAY', 47 | SUNDAY: 'SUNDAY', 48 | }, 49 | zoning: 'Zoning', 50 | }; 51 | -------------------------------------------------------------------------------- /src/shared/DetailsTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import AccessibleReactTable from 'accessible-react-table'; 4 | 5 | const DetailsTable = (props) => { 6 | const numColumns = props.columns.length; 7 | const colWidth = Math.floor(12 / numColumns); 8 | 9 | return ( 10 |
    11 | {props.hasTitle && 12 |
    13 | {props.hasTitleIcon && 14 | props.titleIcon 15 | } {props.title} 16 |
    17 | } 18 |
    19 |
    20 | 27 |
    28 |
    29 |
    30 | ); 31 | }; 32 | 33 | DetailsTable.propTypes = { 34 | hasTitle: PropTypes.bool, 35 | hasTitleIcon: PropTypes.bool, 36 | titleIcon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), 37 | title: PropTypes.string, 38 | columns: PropTypes.array, // eslint-disable-line react/forbid-prop-types 39 | data: PropTypes.array, // eslint-disable-line react/forbid-prop-types 40 | }; 41 | 42 | DetailsTable.defaultProps = { 43 | hasTitle: false, 44 | hasTitleIcon: false, 45 | titleIcon: '', 46 | title: '', 47 | columns: [ 48 | { Header: 'Property/Tax Value', accessor: 'value_type' }, 49 | { Header: 'Amount', accessor: 'amount' }, 50 | ], 51 | data: [ 52 | { value_type: 'Building value', amount: '$682,100' }, 53 | { value_type: 'Land value', amount: '$145,400' }, 54 | { value_type: 'Appraised value', amount: '$827,500' }, 55 | { value_type: 'Tax value', amount: '$0' }, 56 | { value_type: 'Total market value', amount: '$827,500' }, 57 | ], 58 | lastRowBold: false, 59 | }; 60 | 61 | export default DetailsTable; 62 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text eol=lf 23 | *.coffee text 24 | *.json text 25 | *.htm text 26 | *.html text 27 | *.xml text 28 | *.svg text 29 | *.txt text 30 | *.ini text 31 | *.inc text 32 | *.pl text 33 | *.rb text 34 | *.py text 35 | *.scm text 36 | *.sql text 37 | *.sh text 38 | *.bat text 39 | 40 | # templates 41 | *.ejs text 42 | *.hbt text 43 | *.jade text 44 | *.haml text 45 | *.hbs text 46 | *.dot text 47 | *.tmpl text 48 | *.phtml text 49 | 50 | # server config 51 | .htaccess text 52 | 53 | # git config 54 | .gitattributes text 55 | .gitignore text 56 | .gitconfig text 57 | 58 | # code analysis config 59 | .jshintrc text 60 | .jscsrc text 61 | .jshintignore text 62 | .csslintrc text 63 | 64 | # misc config 65 | *.yaml text 66 | *.yml text 67 | .editorconfig text 68 | 69 | # build config 70 | *.npmignore text 71 | *.bowerrc text 72 | 73 | # Heroku 74 | Procfile text 75 | .slugignore text 76 | 77 | # Documentation 78 | *.md text 79 | LICENSE text 80 | AUTHORS text 81 | 82 | 83 | # 84 | ## These files are binary and should be left untouched 85 | # 86 | 87 | # (binary is a macro for -text -diff) 88 | *.png binary 89 | *.jpg binary 90 | *.jpeg binary 91 | *.gif binary 92 | *.ico binary 93 | *.mov binary 94 | *.mp4 binary 95 | *.mp3 binary 96 | *.flv binary 97 | *.fla binary 98 | *.swf binary 99 | *.gz binary 100 | *.zip binary 101 | *.7z binary 102 | *.ttf binary 103 | *.eot binary 104 | *.woff binary 105 | *.pyc binary 106 | *.pdf binary 107 | -------------------------------------------------------------------------------- /src/shared/DetailsIconLinkGrouping.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import DetailsIconLinkFormGroup from './DetailsIconLinkFormGroup'; 4 | 5 | const DetailsIconLinkGrouping = (props) => { 6 | const numArrays = Math.floor(12 / props.colWidth); // step one, determine number of subArrays needed 7 | const subArraySize = Math.floor(props.dataLabels.length / numArrays); 8 | const subLabelsArrays = []; 9 | const subIconsArrays = []; 10 | const subTitlesArrays = []; 11 | const subHrefsArrays = []; 12 | 13 | for (let i = 0, j = props.dataLabels.length; i < j; i += subArraySize) { // step 2, split into subarrays 14 | subLabelsArrays.push(props.dataLabels.slice(i, i + subArraySize)); 15 | subIconsArrays.push(props.dataIcons.slice(i, i + subArraySize)); 16 | subTitlesArrays.push(props.dataTitles.slice(i, i + subArraySize)); 17 | subHrefsArrays.push(props.dataHrefs.slice(i, i + subArraySize)); 18 | } 19 | return ( 20 |
    21 | {subLabelsArrays.map((values, i) => ( 22 |
    23 | {values.map((value, j) => ( 24 | 25 | ))} 26 |
    27 | ))} 28 |
    29 | ); 30 | }; 31 | 32 | DetailsIconLinkGrouping.propTypes = { 33 | colWidth: PropTypes.number, 34 | dataLabels: PropTypes.array, // eslint-disable-line react/forbid-prop-types 35 | dataIcons: PropTypes.array, // eslint-disable-line react/forbid-prop-types 36 | dataHrefs: PropTypes.array, // eslint-disable-line react/forbid-prop-types 37 | dataTitles: PropTypes.array, // eslint-disable-line react/forbid-prop-types 38 | }; 39 | 40 | DetailsIconLinkGrouping.defaultProps = { 41 | colWidth: 12, 42 | dataLabels: [], 43 | dataIcons: [], 44 | dataHrefs: [], 45 | dataTitles: [], 46 | }; 47 | 48 | export default DetailsIconLinkGrouping; 49 | -------------------------------------------------------------------------------- /src/app/budget/graphql/budgetResolvers.js: -------------------------------------------------------------------------------- 1 | import { getSankeyData, getBudgetTrees, getBudgetSummaryDept, getBudgetSummaryUse } from './budgetQueries'; 2 | 3 | export const budgetResolvers = { 4 | Mutation: { 5 | updateSankeyData: (_, { sankeyData }, { cache }) => { 6 | const query = getSankeyData; 7 | const data = { 8 | sankeyData: { 9 | __typename: 'sankeyData', 10 | nodes: sankeyData.nodes, 11 | links: sankeyData.links, 12 | }, 13 | }; 14 | cache.writeQuery({ query, data }); 15 | return data.sankeyData; 16 | }, 17 | updateBudgetTrees: (_, { budgetTrees }, { cache }) => { 18 | const query = getBudgetTrees; 19 | const data = { 20 | budgetTrees: { 21 | __typename: 'budgetTrees', 22 | expenseTree: budgetTrees.expenseTree, 23 | revenueTree: budgetTrees.revenueTree, 24 | expenseTreeForTreemap: budgetTrees.expenseTreeForTreemap, 25 | revenueTreeForTreemap: budgetTrees.revenueTreeForTreemap, 26 | }, 27 | }; 28 | cache.writeQuery({ query, data }); 29 | return data.budgetTrees; 30 | }, 31 | updateBudgetSummaryUse: (_, { budgetSummaryUse }, { cache }) => { 32 | const query = getBudgetSummaryUse; 33 | const data = { 34 | budgetSummaryUse: { 35 | __typename: 'budgetSummaryUse', 36 | dataValues: budgetSummaryUse.dataValues, 37 | dataKeys: budgetSummaryUse.dataKeys, 38 | }, 39 | }; 40 | cache.writeQuery({ query, data }); 41 | return data.budgetSummaryUse; 42 | }, 43 | updateBudgetSummaryDept: (_, { budgetSummaryDept }, { cache }) => { 44 | const query = getBudgetSummaryDept; 45 | const data = { 46 | budgetSummaryDept: { 47 | __typename: 'budgetSummaryDept', 48 | dataValues: budgetSummaryDept.dataValues, 49 | dataKeys: budgetSummaryDept.dataKeys, 50 | }, 51 | }; 52 | cache.writeQuery({ query, data }); 53 | return data.budgetSummaryDept; 54 | }, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/utilities/statistics.js: -------------------------------------------------------------------------------- 1 | 2 | class Statistics { 3 | 4 | inDateRange(inDate, start, end) { 5 | let inRange = true; 6 | const date = new Date(inDate).getTime(); 7 | if ((start && date < start.getTime()) || 8 | (end && date > end.getTime())) { 9 | inRange = false; 10 | } 11 | return inRange; 12 | } 13 | 14 | applyFilters(item, filters) { 15 | let include = true; 16 | let iFilter = 0; 17 | while (include && iFilter < filters.length) { 18 | const { field, type, values } = filters[iFilter]; 19 | if (field in item && item[field]) { 20 | switch (type) { 21 | case 'date_range': 22 | if (!this.inDateRange(item[field], values[0], values[1])) include = false; 23 | break; 24 | 25 | case 'truthy_in_set': 26 | if (!(item[field] in values) || !(values[item[field]])) include = false; 27 | break; 28 | 29 | default: 30 | // Unknown filter, just let it pass through 31 | break; 32 | } 33 | } else { 34 | include = false; 35 | } 36 | ++iFilter; 37 | } 38 | return include; 39 | } 40 | 41 | filter(input, filters) { 42 | const output = []; 43 | input.forEach((item) => { 44 | if (this.applyFilters(item, filters)) output.push(item); 45 | }); 46 | return output; 47 | } 48 | 49 | categoryCounts(data, cFields) { 50 | const counters = {}; 51 | data.forEach((item) => { 52 | cFields.forEach((field) => { 53 | if (!(field in counters)) counters[field] = {}; 54 | const c = item[field]; 55 | if (!(c in counters[field])) counters[field][c] = 0; 56 | ++counters[field][c]; 57 | }); 58 | }); 59 | 60 | const result = {}; 61 | Object.keys(counters).forEach((field) => { 62 | result[field] = []; 63 | Object.keys(counters[field]).forEach((val) => { 64 | result[field].push({ key: val, value: counters[field][val] }); 65 | }); 66 | }); 67 | 68 | return result; 69 | } 70 | } 71 | 72 | export default new Statistics(); 73 | -------------------------------------------------------------------------------- /src/utilities/timeSeriesSet.js: -------------------------------------------------------------------------------- 1 | export default class TimeSeriesSet { 2 | 3 | constructor(series) { 4 | this.series = {}; 5 | if (series) { 6 | series.forEach((name) => { 7 | this.series[name] = { data: [], labels: [], minIndex: Number.MAX_SAFE_INTEGER }; 8 | }); 9 | } 10 | } 11 | 12 | getSeries(name) { 13 | if (name in this.series) return this.series[name]; 14 | return null; 15 | } 16 | 17 | getSeriesData(name) { 18 | if (name in this.series) return this.series[name].data; 19 | return null; 20 | } 21 | 22 | getSeriesLabels(name) { 23 | if (name in this.series) return this.series[name].labels; 24 | return null; 25 | } 26 | 27 | addSeries(name) { 28 | this.series[name] = { data: [], labels: [], minIndex: Number.MAX_SAFE_INTEGER }; 29 | } 30 | 31 | addTimePoint(seriesName, index, value) { 32 | if (!(seriesName in this.series)) this.addSeries(seriesName); 33 | const set = this.series[seriesName]; 34 | if (!(set.data[index])) { 35 | set.data[index] = 0; 36 | set.labels[index] = value; 37 | set.minIndex = Math.min(set.minIndex, index); 38 | } 39 | set.data[index] += 1; 40 | } 41 | 42 | pruneTimeStats(set) { 43 | let newSet = set; 44 | if (set && set.labels && set.labels.length > 1) { 45 | const pdata = set.data; 46 | const plabels = set.labels; 47 | const minIndex = set.minIndex; 48 | newSet = { data: [], labels: [], minIndex }; 49 | let last = null; 50 | pdata.forEach((item, idx) => { 51 | let label = plabels[idx]; 52 | if (label === last) label = ''; 53 | last = plabels[idx]; 54 | newSet.data[idx - minIndex] = item; 55 | newSet.labels[idx - minIndex] = label; 56 | }); 57 | } 58 | return newSet; 59 | } 60 | 61 | finalizeSeries(name = null) { 62 | if (name) { 63 | this.series[name] = this.pruneTimeStats(this.series[name]); 64 | } else { 65 | Object.keys(this.series).forEach((key) => { 66 | this.series[key] = this.pruneTimeStats(this.series[key]); 67 | }); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/shared/visualization/hashid.js: -------------------------------------------------------------------------------- 1 | // Short ID Generation in JavaScript 2 | // http://fiznool.com/blog/2014/11/16/short-id-generation-in-javascript/ 3 | 4 | /** 5 | * The default alphabet is 25 numbers and lowercase letters. 6 | * Any numbers that look like letters and vice versa are removed: 7 | * 1 l, 0 o. 8 | * Also the following letters are not present, to prevent any 9 | * expletives: cfhistu 10 | */ 11 | const DEFAULT_ALPHABET = '23456789abdegjkmnpqrvwxyz'; 12 | 13 | // Governs the length of the ID. 14 | // With an alphabet of 25 chars, 15 | // a length of 8 gives us 25^8 or 16 | // 152,587,890,625 possibilities. 17 | // Should be enough... 18 | const DEFAULT_ID_LENGTH = 5; 19 | 20 | /** 21 | * Governs the number of times we should try to find 22 | * a unique value before giving up. 23 | * @type {Number} 24 | */ 25 | const UNIQUE_RETRIES = 9999; 26 | 27 | /** 28 | * Returns a randomly-generated friendly ID. 29 | * Note that the friendly ID is not guaranteed to be 30 | * unique to any other ID generated by this same method, 31 | * so it is up to you to check for uniqueness. 32 | * @return {String} friendly ID. 33 | */ 34 | export const generate = (options) => { 35 | const { 36 | alphabet = DEFAULT_ALPHABET, 37 | idLength = DEFAULT_ID_LENGTH, 38 | } = Object.assign({}, options); 39 | 40 | let rtn = ''; 41 | for (let i = 0; i < idLength; i++) { 42 | rtn += alphabet.charAt(Math.floor(Math.random() * alphabet.length)); 43 | } 44 | return rtn; 45 | }; 46 | 47 | /** 48 | * Tries to generate a unique ID that is not defined in the 49 | * `previous` array. 50 | * @param {Array} previous The list of previous ids to avoid. 51 | * @return {String} A unique ID, or `null` if one could not be generated. 52 | */ 53 | export const generateUnique = (previous) => { 54 | previous = previous || []; // eslint-disable-line 55 | let retries = 0; 56 | let id; 57 | 58 | // Try to generate a unique ID, 59 | // i.e. one that isn't in the previous. 60 | while (!id && retries < UNIQUE_RETRIES) { 61 | id = generate(); 62 | if (previous.indexOf(id) !== -1) { 63 | id = null; 64 | retries += 1; 65 | } 66 | } 67 | 68 | return id; 69 | }; 70 | -------------------------------------------------------------------------------- /src/app/address/spanish.js: -------------------------------------------------------------------------------- 1 | export const spanish = { 2 | address: 'Dirrecci\xF3n', 3 | addresses_by_street_filename: 'dirrecciones_en_la_calle.csv', 4 | addresses_by_neighborhood_filename: 'dirrecciones_en_el_barrio.csv', 5 | address_and_owner_mailing_lists: 'Listas de correo de direcciones y propietarios', 6 | about_this_address: 'Sobre esta dirrecci\xF3n', 7 | back_to_neighborhood: 'Volver al barrio', 8 | back_to_place_matches: 'Volver a lugares', 9 | back_to_street: 'Volver a la calle', 10 | back_to_search: 'Volver a buscar', 11 | brush_collection: 'Colecci\xF3n de la broza', 12 | brush_week: 'Programa de broza', 13 | data_type: 'Dirrecci\xF3n', 14 | every: 'Cada', 15 | historic_district: 'Distrito hist\xF3rico', 16 | list_view: 'Lista', 17 | local_landmark: 'Punto de referencia local', 18 | map_view: 'Mapa', 19 | neighborhood: 'Barrio', 20 | no_neighborhood_name: 'Barrio sin nombre', 21 | climate: 'Censo Block Group', 22 | no_climate: 'Censo Block Group sin nombre', 23 | no_city_pickup: 'no proporcionada por la ciudad de Asheville', 24 | no_information_available: 'No hay informaci\xF3n disponisble', 25 | no_results_found: 'No se ha encontrado resultados', 26 | of_next_week: 'de la pr\xF3xima semana', 27 | of_this_week: 'de esta semana', 28 | owner: 'Propietario', 29 | placeholder: 'Buscar...', 30 | place_on_curb_by_7am_monday: 'Coloque en el bordillo de la acera antes de las 7 de la ma\xF1ana del lunes', 31 | property_information: 'Detalles de la propiedad', 32 | recycle_week: 'Programa de reciclaje', 33 | recycling_collection: 'Colecci\xF3n de Reciclaje', 34 | report_with_the_asheville_app: 'Reportar con el Asheville App', 35 | sometime_this_week: 'Durante esta semana', 36 | sometime_next_week: 'Durante la pr\xF3xima semana', 37 | street_maintenance: 'Mantenimiento de la calle', 38 | street: 'Calle', 39 | trash_collection: 'Recolecci\xF3n de basura', 40 | weekdays: { 41 | MONDAY: 'LUNES', 42 | TUESDAY: 'MARTES', 43 | WEDNESDAY: 'MI\xC9RCOLES', 44 | THURSDAY: 'JUEVES', 45 | FRIDAY: 'VIERNES', 46 | SATURDAY: 'S\xC1BADO', 47 | SUNDAY: 'DOMINGO', 48 | }, 49 | zoning: 'Zonificaci\xF3n', 50 | }; 51 | -------------------------------------------------------------------------------- /src/app/development/volume/ChildMenus.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | 5 | class ChildMenus extends Component { 6 | render() { 7 | const concatenatedHeritage = this.props.node.heritage 8 | .concat([this.props.node.key]) 9 | .join(); 10 | let className = ''; 11 | if (this.props.node.values) { 12 | className = `${className} dropdown-submenu` 13 | } 14 | let color = 'gray'; 15 | if (this.props.node.selected) { 16 | className = `${className} selected-child-menu-item` 17 | const activeSelected = this.props.activeSelectedNodes.find(candidate => { 18 | if (!candidate.heritage) { return false; } 19 | return candidate.heritage.join() === this.props.node.heritage.join() && 20 | candidate.key === this.props.node.key; 21 | }); 22 | color = activeSelected ? activeSelected.color : 'black'; 23 | } 24 | // TODO: USE REAL X SYMBOL INSTEAD OF LETTER 25 | return ( 26 |
  • 30 | { 34 | e.preventDefault(); 35 | this.props.onNodeClick(this.props.node); 36 | }} 37 | style={{ 38 | color: color, 39 | width: '100%', 40 | }} 41 | role="button" 42 | > 43 | 44 | {this.props.node.key} 45 | 46 | {this.props.node.values &&( 47 |
      51 | {this.props.node.values.map((child) => { 52 | return (); 58 | })} 59 |
    60 | )} 61 |
  • 62 | ) 63 | } 64 | } 65 | 66 | export default ChildMenus; 67 | -------------------------------------------------------------------------------- /src/app/search/searchByEntities/SearchByEntity.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Icon from '../../../shared/Icon'; 4 | import { IM_SHIELD3, IM_OFFICE, IM_ROAD, IM_USER, IM_USERS, IM_LOCATION, IM_HOME2, IM_QUESTION, IM_ARROW_RIGHT2, IM_GOOGLE, IM_LIBRARY2 } from '../../../shared/iconConstants'; 5 | import styles from './searchByEntities.css'; 6 | 7 | const getIcon = (entityType) => { 8 | switch (entityType) { 9 | case 'neighborhood': 10 | return ; 11 | case 'street': 12 | return ; 13 | case 'address': 14 | return ; 15 | case 'owner': 16 | return ; 17 | case 'google': 18 | return ; 19 | case 'property': 20 | return ; 21 | case 'permit': 22 | return ; 23 | default: 24 | return ; 25 | } 26 | }; 27 | 28 | const SearchByEntity = props => ( 29 |
    props.onClick(props.entity.type)}> 30 | 31 | {getIcon(props.entity.type)} 32 | 33 | {props.entity.label} 34 | 35 |
    36 | ); 37 | 38 | const entityDataShape = { 39 | label: PropTypes.string, 40 | type: PropTypes.string, 41 | checked: PropTypes.bool, 42 | }; 43 | 44 | SearchByEntity.propTypes = { 45 | entity: PropTypes.shape(entityDataShape).isRequired, 46 | onClick: PropTypes.func, 47 | }; 48 | 49 | export default SearchByEntity; 50 | 51 | -------------------------------------------------------------------------------- /src/app/development/permits/PermitsMap.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import gql from 'graphql-tag'; 4 | import { Query } from 'react-apollo'; 5 | import { combinePolygonsFromNeighborhoodList } from '../../../utilities/mapUtilities'; 6 | import Map from '../../../shared/visualization/Map'; 7 | import LoadingAnimation from '../../../shared/LoadingAnimation'; 8 | 9 | const GET_NEIGHBORHOODS = gql` 10 | query getNeighborhoodsQuery { 11 | neighborhoods { 12 | name 13 | nbhd_id 14 | abbreviation 15 | narrative 16 | polygon { 17 | outer { 18 | points { 19 | x 20 | y 21 | } 22 | } 23 | holes { 24 | points { 25 | x 26 | y 27 | } 28 | } 29 | } 30 | } 31 | } 32 | `; 33 | 34 | function PermitMap({ 35 | permitData, 36 | centerCoords, 37 | zoom, 38 | showNeighborhoods, 39 | }) { 40 | if (!showNeighborhoods) { 41 | return (); 48 | } 49 | return ( 50 | 53 | {({ loading, error, data }) => { 54 | if (loading) return ; 55 | if (error || data.neighborhoods.length === 0) { 56 | console.log(error); 57 | return
    Error :(
    ; 58 | } 59 | return (); 68 | }} 69 |
    70 | ); 71 | }; 72 | 73 | PermitMap.propTypes = { 74 | permitData: PropTypes.arrayOf(PropTypes.shape({})).isRequired, 75 | centerCoords: PropTypes.arrayOf(PropTypes.number).isRequired, 76 | zoom: PropTypes.number, 77 | showNeighborhoods: PropTypes.bool, 78 | }; 79 | 80 | PermitMap.defaultProps = { 81 | zoom: 12, 82 | showNeighborhoods: false, 83 | }; 84 | 85 | export default PermitMap; 86 | -------------------------------------------------------------------------------- /src/app/development/volume/PermitVolCirclepack.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ResponsiveNetworkFrame } from 'semiotic'; 4 | import Tooltip from '../../../shared/visualization/Tooltip'; 5 | 6 | 7 | const PermitVolCirclepack = props => ( 8 | ({ 19 | stroke: d.color, 20 | fill: d.color, 21 | })} 22 | nodeIDAccessor="key" 23 | hoverAnnotation 24 | // customHoverBehavior={d => { 25 | // if (!d) { 26 | // this.setState({ 27 | // hoverNode: null, 28 | // }); 29 | // return; 30 | // } 31 | // this.setState({ 32 | // hoverNode: d.id, 33 | // }); 34 | // }} 35 | networkType={{ 36 | type: 'circlepack', 37 | hierarchyChildren: d => d.values, 38 | hierarchySum: d => d.value, 39 | // array of data has to be { key: root, values: [...] } 40 | }} 41 | // customClickBehavior={(d) => { 42 | // props.onCircleClick(d.values) 43 | // }} 44 | nodeLabels={(d) => { 45 | if (d.key === 'root' || d.r < 12) { return null; } 46 | return ( 51 | {d.value} 52 | ); 53 | }} 54 | tooltipContent={(d) => { 55 | if (d.key === 'root') { 56 | return ''; 57 | } 58 | const heritage = d.heritage.slice(1); 59 | heritage.push(d.key); 60 | const title = heritage.join(' > '); 61 | return d.key === 'root' ? '' : ( 62 | 66 | ); 67 | }} 68 | />); 69 | 70 | PermitVolCirclepack.propTypes = { 71 | data: PropTypes.object, 72 | }; 73 | 74 | PermitVolCirclepack.defaultProps = { 75 | data: { key: 'root', values: [] }, 76 | }; 77 | 78 | export default PermitVolCirclepack; 79 | -------------------------------------------------------------------------------- /src/gqlClient.js: -------------------------------------------------------------------------------- 1 | import { ApolloClient } from 'apollo-client'; 2 | import fetch from 'unfetch'; 3 | import { createHttpLink } from 'apollo-link-http'; 4 | import { IntrospectionFragmentMatcher, InMemoryCache } from 'apollo-cache-inmemory'; 5 | import { ApolloLink } from 'apollo-link'; 6 | import { withClientState } from 'apollo-link-state'; 7 | import { resolvers } from './resolvers'; 8 | import { defaultState } from './defaultState'; 9 | import { fragmentTypes } from './fragmentTypes'; 10 | 11 | let SERVER_URL = 'https://data-api1.ashevillenc.gov/graphql'; 12 | if (process.env.REACT_APP_USE_DEV_API === true || process.env.REACT_APP_USE_DEV_API === 'true') { 13 | SERVER_URL = 'https://dev-data-api2.ashevillenc.gov/graphql'; 14 | } 15 | if (process.env.REACT_APP_USE_LOCAL_API === true || process.env.REACT_APP_USE_LOCAL_API === 'true') { 16 | SERVER_URL = 'http://localhost:8080/graphql'; 17 | } 18 | 19 | const httpLink = createHttpLink({ uri: SERVER_URL, fetch }); 20 | 21 | // const authLink = setContext( 22 | // request => 23 | // new Promise((success, fail) => { 24 | // const signedInUser = firebase.auth().currentUser; 25 | // if (signedInUser) { 26 | // signedInUser.getIdToken(true) 27 | // .then((idToken) => { 28 | // localStorage.setItem('token', idToken); 29 | // success({ headers: { 30 | // authorization: idToken, 31 | // } }); 32 | // fail(Error(request.statusText)); 33 | // }); 34 | // } else { 35 | // success({ headers: { 36 | // authorization: localStorage.getItem('token') || null, 37 | // } }); 38 | // fail(Error(request.statusText)); 39 | // } 40 | // }) 41 | // ); 42 | 43 | const fragmentMatcher = new IntrospectionFragmentMatcher({ 44 | introspectionQueryResultData: fragmentTypes, 45 | }); 46 | 47 | const cache = new InMemoryCache({ fragmentMatcher }); 48 | 49 | const stateLink = withClientState({ 50 | cache, 51 | defaults: defaultState, 52 | resolvers, 53 | }); 54 | 55 | const aClient = new ApolloClient({ 56 | link: ApolloLink.from([ 57 | stateLink, 58 | httpLink, 59 | ]), 60 | cache, 61 | }); 62 | 63 | aClient.onResetStore(stateLink.writeDefaults); 64 | 65 | export const client = aClient; 66 | 67 | -------------------------------------------------------------------------------- /src/app/development/volume/HierarchicalDropdown.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ChildMenus from './ChildMenus' 4 | 5 | class HierarchicalDropdown extends Component { 6 | /* TODO 7 | props validation 8 | make menus expand on hover of parent menu 9 | make colors match what's happening in parent component 10 | assign them in the depth labels? 11 | both gray vs colorful and opacity 12 | hover style should also match nodes 13 | focus style should match hover style 14 | */ 15 | constructor() { 16 | super() 17 | this.state = { 18 | open: false, 19 | } 20 | this.setWrapperRef = this.setWrapperRef.bind(this); 21 | this.handleClickOutside = this.handleClickOutside.bind(this); 22 | } 23 | 24 | componentDidMount() { 25 | document.addEventListener('mousedown', this.handleClickOutside); 26 | } 27 | 28 | componentWillUnmount() { 29 | document.removeEventListener('mousedown', this.handleClickOutside); 30 | } 31 | 32 | setWrapperRef(node) { 33 | this.wrapperRef = node; 34 | } 35 | 36 | handleClickOutside(event) { 37 | if (this.wrapperRef && !this.wrapperRef.contains(event.target)) { 38 | this.setState({ open: false }) 39 | } 40 | } 41 | 42 | render() { 43 | return (
    47 | 59 |
      63 | {this.props.hierarchy.values.map(node => ( 64 | 70 | ))} 71 |
    72 |
    ) 73 | } 74 | } 75 | 76 | export default HierarchicalDropdown; 77 | -------------------------------------------------------------------------------- /src/app/development/volume/dotBinLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { scaleLinear } from 'd3-scale'; 3 | 4 | export default function dotBinLayout({ 5 | type, 6 | data, 7 | styleFn, 8 | projection, 9 | classFn, 10 | adjustedSize 11 | }) { 12 | const keys = Object.keys(data) 13 | let allCalculatedPieces = [] 14 | keys.forEach(key => { 15 | const ordset = data[key]; 16 | const radiusFunc = scaleLinear() 17 | .range([2, type.maxRadius]) 18 | .domain([0, type.maxRadiusVal]); 19 | const calculatedPieces = ordset.pieceData 20 | .filter(pieceDatum => pieceDatum.data.count > 0) 21 | .map((piece, i) => { 22 | 23 | const radius = radiusFunc(piece.data.count) 24 | const pieceSize = radius * 2; 25 | let xPosition = piece.scaledValue 26 | let yPosition = ordset.middle - radius 27 | let finalWidth = pieceSize 28 | let finalHeight = pieceSize 29 | 30 | if (!piece.negative) { 31 | yPosition -= piece.scaledValue 32 | } 33 | 34 | if (projection === "horizontal") { 35 | yPosition = ordset.middle - radius 36 | xPosition = piece.scaledValue 37 | if (piece.negative) { 38 | xPosition = piece.scaledValue - piece.scaledValue 39 | } 40 | } 41 | 42 | const xy = { 43 | x: xPosition, 44 | y: yPosition, 45 | middle: radius, 46 | height: finalHeight, 47 | width: finalWidth 48 | } 49 | 50 | const renderElementObject = ( 51 | 58 | 62 | 63 | ) 64 | 65 | const calculatedPiece = { 66 | o: key, 67 | xy, 68 | piece, 69 | renderElement: renderElementObject 70 | } 71 | return calculatedPiece 72 | }) 73 | allCalculatedPieces = [...allCalculatedPieces, ...calculatedPieces] 74 | }) 75 | return allCalculatedPieces 76 | } 77 | -------------------------------------------------------------------------------- /src/utilities/dateUtilities.js: -------------------------------------------------------------------------------- 1 | // TODO: convert to functions 2 | 3 | 4 | class DateUtilities { 5 | // Returns the ISO week of the date. From https://weeknumber.net/how-to/javascript 6 | getWeek(dd) { 7 | const date = new Date(dd.getTime()); 8 | date.setHours(0, 0, 0, 0); 9 | // Thursday in current week decides the year. 10 | date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); // eslint-disable-line no-mixed-operators 11 | // January 4 is always in week 1. 12 | const week1 = new Date(date.getFullYear(), 0, 4); 13 | // Adjust to Thursday in week 1 and count number of weeks from date to week1. 14 | return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 // eslint-disable-line no-mixed-operators 15 | - 3 + (week1.getDay() + 6) % 7) / 7); // eslint-disable-line no-mixed-operators 16 | } 17 | 18 | // Returns the four-digit year corresponding to the ISO week of the date. 19 | getWeekYear(dd) { 20 | const date = new Date(dd.getTime()); 21 | date.setDate((date.getDate() + 3) - (date.getDay() + 6) % 7); // eslint-disable-line no-mixed-operators 22 | return date.getFullYear(); 23 | } 24 | 25 | dateIndex(date, mode) { 26 | const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 27 | const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 28 | let idx = null; 29 | if (mode === 'month') { 30 | const m = date.getMonth(); 31 | idx = { index: m, value: months[m] }; 32 | } else if (mode === 'week') { 33 | const week = this.getWeek(date); 34 | const dd = new Date(date.getTime()); 35 | dd.setHours(0, 0, 0, 0); 36 | dd.setDate(dd.getDate() - (dd.getDay() + 6) % 7); // eslint-disable-line no-mixed-operators 37 | idx = { index: week, value: months[dd.getMonth()] }; 38 | } else if (mode === 'day-of-week') { 39 | const day = date.getDay(); 40 | idx = { index: day, value: days[day] }; 41 | } 42 | return idx; 43 | } 44 | 45 | inDateRange(inDate, start, end) { 46 | let inRange = true; 47 | const date = new Date(inDate).getTime(); 48 | if ((start && date < start.getTime()) || 49 | (end && date > end.getTime())) { 50 | inRange = false; 51 | } 52 | return inRange; 53 | } 54 | } 55 | 56 | export default new DateUtilities(); 57 | -------------------------------------------------------------------------------- /src/app/development/volume/PermitTypeMenus.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | 5 | const PermitTypeMenus = props => ( 6 |
    7 | {props.parentHierarchyLevels.map((level, levelIndex, array) => { 8 | // If the level before it has no selection, don't show it 9 | if (levelIndex > 0 && array[levelIndex - 1].selectedCat === null) { 10 | return null; 11 | } 12 | let keyLevel = props.wholeHierarchy; 13 | for (let index = 0; index < levelIndex; index++) { 14 | keyLevel = keyLevel[array[index].selectedCat]; 15 | } 16 | /* 17 | * If the value is not null, make it a selected dropdown 18 | * If someone changes the selected dropdown of 19 | * one earlier in the array, the later one's value should be cleared 20 | */ 21 | const orderedKeys = Object.keys(keyLevel).sort((a, b) => { 22 | if (a > b) { 23 | return 1; 24 | } else if (a < b) { 25 | return -1; 26 | } 27 | return 0; 28 | }); 29 | 30 | return (
    39 |
    40 | {`${level.name.replace('_', ' ')}: `}  41 |
    42 | 57 |
    ); 58 | })} 59 |
    60 | ); 61 | 62 | PermitTypeMenus.propTypes = { 63 | onSelect: PropTypes.func, 64 | parentHierarchyLevels: PropTypes.arrayOf(PropTypes.object), 65 | wholeHierarchy: PropTypes.object, 66 | }; 67 | 68 | PermitTypeMenus.defaultProps = { 69 | onSelect: e => console.log(e.target.value), 70 | parentHierarchyLevels: {}, 71 | wholeHierarchy: {}, 72 | }; 73 | 74 | export default PermitTypeMenus; 75 | -------------------------------------------------------------------------------- /src/shared/react_table_hoc/ExpandingRows.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import mergeProps from 'merge-prop-functions'; 3 | 4 | export default function expandingRows(WrappedReactTable) { 5 | class ExpandableRowsReactTable extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.getCustomTrProps = this.getCustomTrProps.bind(this); 9 | this.onExpandedChange = this.onExpandedChange.bind(this); 10 | this.onSortedChange = this.onSortedChange.bind(this); 11 | this.onFilteredChange = this.onFilteredChange.bind(this); 12 | this.onPageChange = this.onPageChange.bind(this); 13 | this.state = { 14 | expanded: {}, 15 | }; 16 | } 17 | 18 | onExpandedChange(expanded) { 19 | this.setState({ expanded }); 20 | } 21 | 22 | onSortedChange() { 23 | this.setState({ 24 | expanded: {}, 25 | }); 26 | } 27 | 28 | onFilteredChange() { 29 | this.setState({ 30 | expanded: {}, 31 | }); 32 | } 33 | 34 | onPageChange() { 35 | this.setState({ 36 | expanded: {}, 37 | }); 38 | } 39 | 40 | getCustomTrProps(state, rowInfo) { 41 | return { 42 | onClick: () => { 43 | const expanded = Object.assign({}, this.state.expanded); 44 | expanded[rowInfo.viewIndex] = !this.state.expanded[rowInfo.viewIndex]; 45 | this.setState({ expanded }); 46 | }, 47 | }; 48 | } 49 | 50 | render() { 51 | const newProps = Object.assign({}, this.props); 52 | const getTdProps = newProps.getTrProps; 53 | 54 | let newGetTrProps; 55 | if (getTdProps) { 56 | newGetTrProps = mergeProps(this.getCustomTrProps, getTdProps); 57 | delete newProps.getTrProps; 58 | } else { 59 | newGetTrProps = this.getCustomTrProps; 60 | } 61 | 62 | return ( 63 | 72 | ); 73 | } 74 | } 75 | 76 | ExpandableRowsReactTable.propTypes = WrappedReactTable.propTypes; 77 | return ExpandableRowsReactTable; 78 | } 79 | -------------------------------------------------------------------------------- /src/app/search/searchByEntities/SearchByEntities.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import SearchByEntity from './SearchByEntity'; 4 | import styles from './searchByEntities.css'; 5 | import { refreshLocation } from '../../../utilities/generalUtilities'; 6 | 7 | const SearchByEntities = (props) => { 8 | const getNewUrlParams = (entity) => { 9 | let newSelected = ''; 10 | const useLocation = props.location.query.entities !== undefined; 11 | const curSelected = (useLocation ? props.location.query.entities : props.selectedEntities).split(','); 12 | const alreadySelected = curSelected.indexOf(entity) > -1; 13 | if (alreadySelected) { 14 | newSelected = curSelected.filter(ent => ent !== entity); 15 | } else { 16 | newSelected = [curSelected, entity].join(',').replace(/(^,)|(,$)/g, ''); 17 | } 18 | return { 19 | search: document.getElementById('searchBox').value, 20 | entities: newSelected, 21 | }; 22 | }; 23 | 24 | return ( 25 |
    26 | Entities to search by 27 |
      28 | {props.entities.map((entity, i) => ( 29 |
    • 30 | refreshLocation(getNewUrlParams(entity.type), props.location)} /> 31 |
    • 32 | ))} 33 |
    34 |
    35 | ); 36 | }; 37 | 38 | const entityDataShape = { 39 | label: PropTypes.String, 40 | entityType: PropTypes.string, 41 | checked: PropTypes.bool, 42 | }; 43 | 44 | SearchByEntities.propTypes = { 45 | entities: PropTypes.arrayOf(PropTypes.shape(entityDataShape)), 46 | selectedEntities: PropTypes.string, 47 | }; 48 | 49 | SearchByEntities.defaultProps = { 50 | entities: [ 51 | { label: 'Addresses', type: 'address', checked: true }, 52 | { label: 'Properties', type: 'property', checked: true }, 53 | { label: 'Neighborhoods', type: 'neighborhood', checked: true }, 54 | { label: 'Streets', type: 'street', checked: true }, 55 | { label: 'Owners', type: 'owner', checked: true }, 56 | // { label: 'Permits', type: 'permit', checked: true }, 57 | // { label: 'Google places', type: 'google', checked: true }, 58 | ], 59 | selectedEntities: '', 60 | }; 61 | 62 | export default SearchByEntities; 63 | 64 | -------------------------------------------------------------------------------- /src/app/development/volume/GranularDash.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { stackedHistogramFromNodes } from './granularUtils'; 4 | import BooleanSplitMultiples from './BooleanSplitMultiples'; 5 | import LoadingAnimation from '../../../shared/LoadingAnimation'; 6 | import PermitVolCirclepack from './PermitVolCirclepack'; 7 | import VolumeHistogram from './VolumeHistogram'; 8 | 9 | 10 | const GranularDash = (props) => { 11 | const histData = props.selectedNodes ? 12 | stackedHistogramFromNodes(props.selectedNodes, props.timeSpan) : 13 | []; 14 | 15 | const totalCount = props.selectedData.length; 16 | const circlePackData = { 17 | key: 'root', 18 | color: 'none', 19 | heritage: [], 20 | values: props.selectedNodes ? 21 | props.selectedNodes.map(node => 22 | ({ 23 | color: node.color, 24 | heritage: node.heritage, 25 | key: node.key, 26 | selected: node.selected, 27 | value: node.selectedActiveValues.length, 28 | })) : 29 | [], 30 | }; 31 | 32 | const subTitle = props.selectedHierarchyTitle ? 33 | `Volume by Record ${props.selectedHierarchyTitle}` : 34 | 'Volume'; 35 | 36 | return (
    37 |
    41 |

    {subTitle}

    42 |
    43 |

    Daily

    44 | 48 |
    49 |
    50 |

    {`Total: ${totalCount}`}

    51 | {props.selectedNodes ? 52 | ( this.onModalOpen(circleData)} 56 | />) : 57 | 58 | } 59 |
    60 |
    61 |
    62 |

    Online vs In Person

    63 | {/* Make shared extent with FacetController after issue is fixed */} 64 | {props.selectedNodes ? 65 | ( !node.othered)} 69 | />) : 70 | 71 | } 72 |
    73 |
    ); 74 | } 75 | 76 | export default GranularDash; 77 | -------------------------------------------------------------------------------- /src/styles/bootstrap/_pagination.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Pagination (multiple pages) 3 | // -------------------------------------------------- 4 | .pagination { 5 | display: inline-block; 6 | padding-left: 0; 7 | margin: $line-height-computed 0; 8 | border-radius: $border-radius-base; 9 | 10 | > li { 11 | display: inline; // Remove list-style and block-level defaults 12 | > a, 13 | > span { 14 | position: relative; 15 | float: left; // Collapse white-space 16 | padding: $padding-base-vertical $padding-base-horizontal; 17 | line-height: $line-height-base; 18 | text-decoration: none; 19 | color: $pagination-color; 20 | background-color: $pagination-bg; 21 | border: 1px solid $pagination-border; 22 | margin-left: -1px; 23 | } 24 | &:first-child { 25 | > a, 26 | > span { 27 | margin-left: 0; 28 | @include border-left-radius($border-radius-base); 29 | } 30 | } 31 | &:last-child { 32 | > a, 33 | > span { 34 | @include border-right-radius($border-radius-base); 35 | } 36 | } 37 | } 38 | 39 | > li > a, 40 | > li > span { 41 | &:hover, 42 | &:focus { 43 | z-index: 2; 44 | color: $pagination-hover-color; 45 | background-color: $pagination-hover-bg; 46 | border-color: $pagination-hover-border; 47 | } 48 | } 49 | 50 | > .active > a, 51 | > .active > span { 52 | &, 53 | &:hover, 54 | &:focus { 55 | z-index: 3; 56 | color: $pagination-active-color; 57 | background-color: $pagination-active-bg; 58 | border-color: $pagination-active-border; 59 | cursor: default; 60 | } 61 | } 62 | 63 | > .disabled { 64 | > span, 65 | > span:hover, 66 | > span:focus, 67 | > a, 68 | > a:hover, 69 | > a:focus { 70 | color: $pagination-disabled-color; 71 | background-color: $pagination-disabled-bg; 72 | border-color: $pagination-disabled-border; 73 | cursor: $cursor-disabled; 74 | } 75 | } 76 | } 77 | 78 | // Sizing 79 | // -------------------------------------------------- 80 | 81 | // Large 82 | .pagination-lg { 83 | @include pagination-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $border-radius-large); 84 | } 85 | 86 | // Small 87 | .pagination-sm { 88 | @include pagination-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $border-radius-small); 89 | } 90 | --------------------------------------------------------------------------------