├── .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 |
10 |
11 | );
12 | }
13 |
14 | LinkButton.propTypes = {
15 | pathname: PropTypes.string,
16 | query: PropTypes.object, // eslint-disable-line
17 | };
18 |
19 | export default LinkButton;
20 |
--------------------------------------------------------------------------------
/src/app/development/trc/SubNode.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NodeSteps from './NodeSteps';
3 |
4 | const SubNode = ({ node }) => (
5 |
17 | );
18 |
19 | export default SubNode;
20 |
--------------------------------------------------------------------------------
/src/shared/ButtonGroup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const ButtonGroup = props => (
5 |
6 | { props.children }
7 |
8 | );
9 |
10 | ButtonGroup.propTypes = {
11 | children: PropTypes.node,
12 | alignment: PropTypes.string,
13 | style: PropTypes.object, // eslint-disable-line
14 | };
15 |
16 | ButtonGroup.defaultProps = {
17 | // alignment: 'right',
18 | };
19 |
20 | export default ButtonGroup;
21 |
--------------------------------------------------------------------------------
/src/app/development/volume/LoadingModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const LoadingModal = (props) => (
4 |
17 | )
18 |
19 | // TODO: ADD PROPS
20 | // OPEN
21 |
22 | export default LoadingModal;
23 |
--------------------------------------------------------------------------------
/src/utilities/auth/graphql/authQueries.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | export const getUser = gql`
4 | query getUser {
5 | user @client {
6 | loggedIn
7 | privilege
8 | name
9 | email
10 | provider
11 | }
12 | }
13 | `;
14 |
15 | export const getModalOpen = gql`
16 | query getModalOpen {
17 | modal @client {
18 | open
19 | }
20 | }
21 | `;
22 |
23 | export const getDropdownOpen = gql`
24 | query getDropdownOpen {
25 | dropdown @client {
26 | open
27 | }
28 | }
29 | `;
30 |
--------------------------------------------------------------------------------
/docs/development-resources/readme.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 | # Development Resources
5 |
6 | ## GraphQL
7 |
8 | Here’s a really good [intro video](https://www.youtube.com/watch?v=Wq02BNrN1dU)
9 |
10 | And here’s the [docs](http://graphql.org/docs/getting-started/)
11 |
12 | [Some additional reading](https://medium.com/apollo-stack/graphql-the-next-generation-of-api-design-f24b1689756a#.bk3a14at1)
13 |
14 | ## State Management with Apollo
15 |
16 | Application state is being managed with Apollo Client. See [docs] (https://github.com/apollographql/apollo-link-state).
--------------------------------------------------------------------------------
/src/styles/bootstrap/mixins/_reset-text.scss:
--------------------------------------------------------------------------------
1 | @mixin reset-text() {
2 | font-family: $font-family-base;
3 | // We deliberately do NOT reset font-size.
4 | font-style: normal;
5 | font-weight: normal;
6 | letter-spacing: normal;
7 | line-break: auto;
8 | line-height: $line-height-base;
9 | text-align: left; // Fallback for where `start` is not supported
10 | text-align: start;
11 | text-decoration: none;
12 | text-shadow: none;
13 | text-transform: none;
14 | white-space: normal;
15 | word-break: normal;
16 | word-spacing: normal;
17 | word-wrap: normal;
18 | }
19 |
--------------------------------------------------------------------------------
/src/styles/bootstrap/mixins/_border-radius.scss:
--------------------------------------------------------------------------------
1 | // Single side border-radius
2 |
3 | @mixin border-top-radius($radius) {
4 | border-top-right-radius: $radius;
5 | border-top-left-radius: $radius;
6 | }
7 | @mixin border-right-radius($radius) {
8 | border-bottom-right-radius: $radius;
9 | border-top-right-radius: $radius;
10 | }
11 | @mixin border-bottom-radius($radius) {
12 | border-bottom-right-radius: $radius;
13 | border-bottom-left-radius: $radius;
14 | }
15 | @mixin border-left-radius($radius) {
16 | border-bottom-left-radius: $radius;
17 | border-top-left-radius: $radius;
18 | }
19 |
--------------------------------------------------------------------------------
/src/styles/components/DetailsTable.scss:
--------------------------------------------------------------------------------
1 | .titleDiv {
2 | margin-bottom: 5px;
3 | margin-left: 10px;
4 | }
5 |
6 | .titleDiv i {
7 | font-size: 25px;
8 | vertical-align: middle;
9 | margin-right: 5px;
10 | }
11 |
12 | .headerRow {
13 | border-bottom: 2px solid #ddd;
14 | margin-right: 15px;
15 | margin-left: 15px;
16 | margin-bottom: 10px;
17 | margin-top: 10px;
18 | }
19 |
20 | .columnHeader {
21 | font-weight: bold;
22 | }
23 |
24 | .columnHeader:first-of-type {
25 | margin-left: -15px;
26 | margin-right: 15px;
27 | }
28 |
29 | .columnData {
30 | border-bottom: 1px solid #ddd;
31 | }
--------------------------------------------------------------------------------
/src/app/budget/graphql/budgetDefaultState.js:
--------------------------------------------------------------------------------
1 | export const defaultBudgetState = {
2 | sankeyData: {
3 | __typename: 'sankeyData',
4 | nodes: null,
5 | links: null,
6 | },
7 | budgetTrees: {
8 | __typename: 'budgetTrees',
9 | expenseTree: null,
10 | revenueTree: null,
11 | expenseTreeForTreemap: null,
12 | revenueTreeForTreemap: null,
13 | },
14 | budgetSummaryUse: {
15 | __typename: 'budgetSummaryUse',
16 | dataKeys: null,
17 | dataValues: null,
18 | },
19 | budgetSummaryDept: {
20 | __typename: 'budgetSummaryDept',
21 | dataKeys: null,
22 | dataValues: null,
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/src/app/search/styles.css:
--------------------------------------------------------------------------------
1 | .combobox-item {
2 | display: flex;
3 | cursor: default;
4 | scroll-margin: 0.5rem;
5 | align-items: center;
6 | gap: 0.5rem;
7 | border-radius: 0.25rem;
8 | padding: 0.5rem;
9 | outline: none !important;
10 | }
11 |
12 | .combobox-item:hover {
13 | background: #f6fcff;
14 | }
15 |
16 |
17 | .combobox-item[data-active-item] {
18 | background-color: #c3ebff;
19 | /* color: hsl(204 20% 100%); */
20 | }
21 |
22 | .suggestion-text {
23 | line-height: 1;
24 | text-transform: capitalize;
25 | margin-left: 2px;
26 | }
27 |
28 | .suggestion-icon {
29 | color: #4077a5;
30 | /* color: purple; */
31 | }
--------------------------------------------------------------------------------
/src/styles/bootstrap/mixins/_pagination.scss:
--------------------------------------------------------------------------------
1 | // Pagination
2 |
3 | @mixin pagination-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) {
4 | > li {
5 | > a,
6 | > span {
7 | padding: $padding-vertical $padding-horizontal;
8 | font-size: $font-size;
9 | line-height: $line-height;
10 | }
11 | &:first-child {
12 | > a,
13 | > span {
14 | @include border-left-radius($border-radius);
15 | }
16 | }
17 | &:last-child {
18 | > a,
19 | > span {
20 | @include border-right-radius($border-radius);
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/styles/bootstrap/mixins/_responsive-visibility.scss:
--------------------------------------------------------------------------------
1 | // Responsive utilities
2 |
3 | //
4 | // More easily include all the states for responsive-utilities.less.
5 | // [converter] $parent hack
6 | @mixin responsive-visibility($parent) {
7 | #{$parent} {
8 | display: block !important;
9 | }
10 | table#{$parent} { display: table !important; }
11 | tr#{$parent} { display: table-row !important; }
12 | th#{$parent},
13 | td#{$parent} { display: table-cell !important; }
14 | }
15 |
16 | // [converter] $parent hack
17 | @mixin responsive-invisibility($parent) {
18 | #{$parent} {
19 | display: none !important;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/styles/components/search-result.scss:
--------------------------------------------------------------------------------
1 | .search-result{
2 | border: 1px solid #9E9E9E;
3 | border-radius: 4px;
4 | width: 100%;
5 | height: 60px;
6 | font-size: 30px;
7 | padding-top: 10px;
8 | margin-top: 10px;
9 | background: #FFF;
10 | }
11 |
12 | .search-result.more{
13 | color: $brand-primary;
14 | text-align: left;
15 | outline: none;
16 | }
17 |
18 | .search-result:hover{
19 | 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);
20 | }
21 |
22 | .search-result-icon{
23 | margin-right: 20px;
24 | }
25 |
26 | .search-result-arrow-icon{
27 | float: right;
28 | margin-top: 6px;
29 | }
--------------------------------------------------------------------------------
/src/styles/bootstrap/mixins/_panels.scss:
--------------------------------------------------------------------------------
1 | // Panels
2 |
3 | @mixin panel-variant($border, $heading-text-color, $heading-bg-color, $heading-border) {
4 | border-color: $border;
5 |
6 | & > .panel-heading {
7 | color: $heading-text-color;
8 | background-color: $heading-bg-color;
9 | border-color: $heading-border;
10 |
11 | + .panel-collapse > .panel-body {
12 | border-top-color: $border;
13 | }
14 | .badge {
15 | color: $heading-bg-color;
16 | background-color: $heading-text-color;
17 | }
18 | }
19 | & > .panel-footer {
20 | + .panel-collapse > .panel-body {
21 | border-bottom-color: $border;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/styles/bootstrap/_wells.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Wells
3 | // --------------------------------------------------
4 |
5 |
6 | // Base class
7 | .well {
8 | min-height: 20px;
9 | padding: 19px;
10 | margin-bottom: 20px;
11 | background-color: $well-bg;
12 | border: 1px solid $well-border;
13 | border-radius: $border-radius-base;
14 | @include box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
15 | blockquote {
16 | border-color: #ddd;
17 | border-color: rgba(0,0,0,.15);
18 | }
19 | }
20 |
21 | // Sizes
22 | .well-lg {
23 | padding: 24px;
24 | border-radius: $border-radius-large;
25 | }
26 | .well-sm {
27 | padding: 9px;
28 | border-radius: $border-radius-small;
29 | }
30 |
--------------------------------------------------------------------------------
/docs/notes/readme.md:
--------------------------------------------------------------------------------
1 | ## Keeping code clean and improving the development experience
2 | * eslint (lints your JS: install VS Code extension to get this to work)
3 | * stylelint (lints your CSS: install VS Code extension to get this to work)
4 | * editorconfig (sets some defaults for your text editor (ident size and what not): (install VS Code extension to get this to work)
5 |
6 | ##Todo
7 |
8 | 1. If you want to make this a progressive web app:
9 | - Update the manifest.json file to allow user to install to home screen
10 | - Add ` `
11 | - Add ` `
12 | 2. Helmet is a really good idea for SEO
13 |
14 |
--------------------------------------------------------------------------------
/src/styles/bootstrap/mixins/_hide-text.scss:
--------------------------------------------------------------------------------
1 | // CSS image replacement
2 | //
3 | // Heads up! v3 launched with only `.hide-text()`, but per our pattern for
4 | // mixins being reused as classes with the same name, this doesn't hold up. As
5 | // of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`.
6 | //
7 | // Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757
8 |
9 | // Deprecated as of v3.0.1 (has been removed in v4)
10 | @mixin hide-text() {
11 | font: 0/0 a;
12 | color: transparent;
13 | text-shadow: none;
14 | background-color: transparent;
15 | border: 0;
16 | }
17 |
18 | // New mixin to use as of v3.0.1
19 | @mixin text-hide() {
20 | @include hide-text;
21 | }
22 |
--------------------------------------------------------------------------------
/src/shared/GetVersion.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import gql from 'graphql-tag';
3 | import { Query } from 'react-apollo';
4 |
5 | const VERSION = gql`
6 | query version {
7 | version
8 | }
9 | `;
10 |
11 | const GetVersion = () => {
12 | return (
13 |
16 | {({ loading, error, data }) => {
17 | if (error) {
18 | console.log(error);
19 | return null;
20 | }
21 |
22 | if (loading) {
23 | return null;
24 | }
25 |
26 | console.log('api version: ', data.version);
27 |
28 | return null;
29 | }}
30 |
31 | );
32 | };
33 |
34 | export default GetVersion;
--------------------------------------------------------------------------------
/src/styles/components/home.scss:
--------------------------------------------------------------------------------
1 | .homeTitle {
2 | font-weight: 200;
3 | color: #4579B3;
4 | font-size: 60px;
5 | margin: 0px;
6 | margin-top: 100px;
7 | margin-bottom: 20px;
8 | text-align: center;
9 | }
10 |
11 | .bigNavWrapper {
12 | margin-top: 100px;
13 | }
14 |
15 | .bigNavButton {
16 | background: #FFF;
17 | color: #9E9E9E;
18 | font-weight: 500;
19 | height: 80px;
20 | text-align: center;
21 | vertical-align: middle;
22 | padding: 25px;
23 | margin: 10px;
24 | font-size: 18px;
25 | border-radius: 4px;
26 | box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.4);
27 | }
28 |
29 | .bigNavButton:hover {
30 | box-shadow: 3px 3px 9px rgba(0, 0, 0, 0.4);
31 | margin-top: -1px;
32 | cursor: pointer;
33 | }
--------------------------------------------------------------------------------
/src/styles/components/search.scss:
--------------------------------------------------------------------------------
1 | .search {
2 | position: absolute;
3 | top: 0px;
4 | bottom: 0px;
5 | right: 0px;
6 | left: 0px;
7 | background: #FFF;
8 | }
9 |
10 | .search-close {
11 | font-size: 50px;
12 | background: #FFF;
13 | color: $brand-primary;
14 | border: none;
15 | position: absolute;
16 | top: 5px;
17 | right: 30px;
18 | outline: none;
19 | line-height: 1;
20 | border-radius: 4px;
21 | height: 50px;
22 | width: 50px;
23 | text-align: center;
24 | cursor: pointer;
25 | }
26 |
27 | .search-close:hover{
28 | 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);
29 | }
30 |
31 | .search-bar-wrapper {
32 | margin-top: 60px;
33 | }
--------------------------------------------------------------------------------
/src/styles/components/PermitTimeline.scss:
--------------------------------------------------------------------------------
1 | /* width */
2 |
3 | #permit-timeline-container {
4 | margin: 1rem 0;
5 | padding: 0.25rem;
6 | border-radius: 6px;
7 | height: 165px;
8 | width: 100%;
9 | overflow-x: auto;
10 | overflow-y: hidden;
11 | position: relative;
12 | }
13 |
14 | #permit-timeline-container::-webkit-scrollbar {
15 | height: 4px;
16 | }
17 |
18 | /* Track */
19 | #permit-timeline-container::-webkit-scrollbar-track {
20 | box-shadow: inset 0 0 2px #e6e6e6;
21 | }
22 |
23 | /* Handle */
24 | #permit-timeline-container::-webkit-scrollbar-thumb {
25 | background: #e6e6e6;
26 | }
27 |
28 | @media(min-width: 767px){
29 | #permit-timeline-container {
30 | /* min-height: 145px; */
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/styles/bootstrap/_responsive-embed.scss:
--------------------------------------------------------------------------------
1 | // Embeds responsive
2 | //
3 | // Credit: Nicolas Gallagher and SUIT CSS.
4 |
5 | .embed-responsive {
6 | position: relative;
7 | display: block;
8 | height: 0;
9 | padding: 0;
10 | overflow: hidden;
11 |
12 | .embed-responsive-item,
13 | iframe,
14 | embed,
15 | object,
16 | video {
17 | position: absolute;
18 | top: 0;
19 | left: 0;
20 | bottom: 0;
21 | height: 100%;
22 | width: 100%;
23 | border: 0;
24 | }
25 | }
26 |
27 | // Modifier class for 16:9 aspect ratio
28 | .embed-responsive-16by9 {
29 | padding-bottom: 56.25%;
30 | }
31 |
32 | // Modifier class for 4:3 aspect ratio
33 | .embed-responsive-4by3 {
34 | padding-bottom: 75%;
35 | }
36 |
--------------------------------------------------------------------------------
/src/styles/bootstrap/mixins/_clearfix.scss:
--------------------------------------------------------------------------------
1 | // Clearfix
2 | //
3 | // For modern browsers
4 | // 1. The space content is one way to avoid an Opera bug when the
5 | // contenteditable attribute is included anywhere else in the document.
6 | // Otherwise it causes space to appear at the top and bottom of elements
7 | // that are clearfixed.
8 | // 2. The use of `table` rather than `block` is only necessary if using
9 | // `:before` to contain the top-margins of child elements.
10 | //
11 | // Source: http://nicolasgallagher.com/micro-clearfix-hack/
12 |
13 | @mixin clearfix() {
14 | &:before,
15 | &:after {
16 | content: " "; // 1
17 | display: table; // 2
18 | }
19 | &:after {
20 | clear: both;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/fragmentTypes.js:
--------------------------------------------------------------------------------
1 | export const fragmentTypes = {
2 | __schema: {
3 | types: [
4 | {
5 | kind: 'INTERFACE',
6 | name: 'SearchResult',
7 | possibleTypes: [
8 | {
9 | name: 'SillyResult',
10 | },
11 | {
12 | name: 'AddressResult',
13 | },
14 | {
15 | name: 'PropertyResult',
16 | },
17 | {
18 | name: 'StreetResult',
19 | },
20 | {
21 | name: 'OwnerResult',
22 | },
23 | {
24 | name: 'NeighborhoodResult',
25 | },
26 | {
27 | name: 'PlaceResult',
28 | },
29 | ],
30 | },
31 | ],
32 | },
33 | };
34 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: Simplicity II
4 | ---
5 | # SimpliCity II
6 |
7 | ## Table of Contents
8 |
9 | - [Requirements & Getting Started](./requirements/principles)
10 | - [Resources](./development-resources/readme)
11 | - [Testing](./testing/readme)
12 |
13 | ## Overview
14 |
15 | SimpliCity allows the community to find useful information about specific addresses and properties, such as trash and recycling pickup days, property values, and crimes or development near an address. It also provides city-wide dashboards about topics like homelessness, the City budget, and capital projects spending.
16 |
17 | ## Future Features that we have in mind
18 |
19 | * My SimpliCity
20 | * Social login allowing you to save bookmarks and favorites
21 | * City Staff Role-based Topic Dashboards
22 |
--------------------------------------------------------------------------------
/src/styles/components/leaflet.scss:
--------------------------------------------------------------------------------
1 | .leaflet-container {
2 | height: 100%;
3 | }
4 |
5 | .leaflet-container * {
6 | z-index: 0;
7 | }
8 |
9 | .legend {
10 | display: none;
11 | position: absolute;
12 | bottom: 0px;
13 | right: 0px;
14 | background: white;
15 | border: 2px solid #c2bfba;
16 | border-radius: 4px;
17 | padding: 10px;
18 | overflow-y: auto;
19 | }
20 |
21 | .legendIcon {
22 | background: white;
23 | border: 2px solid #c2bfba;
24 | border-radius: 4px;
25 | padding: 10px;
26 | cursor: pointer;
27 | }
28 |
29 | .closeLegend {
30 | position: absolute;
31 | right: 5px;
32 | top: 3px;
33 | cursor: pointer;
34 | }
35 |
36 | .closeLegend:hover {
37 | color: #237cc9 !important;
38 | }
39 |
40 | .noPointer {
41 | cursor: -webkit-grab;
42 | cursor: -moz-grab;
43 | }
44 |
--------------------------------------------------------------------------------
/src/shared/FilterRenderer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CellFocusWrapper } from 'accessible-react-table';
3 |
4 | // eslint-disable-next-line react/prop-types
5 | const createFilterRenderer = (placeholderText, extraProps) => ({ filter, onChange }) => (
6 |
7 | {(focusRef, focusable) => (
8 | onChange(event.target.value)}
10 | style={{ width: '100%' }}
11 | value={filter ? filter.value : ''}
12 | className={`table-filter${filter && filter.value ? ' filter-active' : ''}`}
13 | placeholder={placeholderText}
14 | tabIndex={focusable ? 0 : -1}
15 | ref={focusRef}
16 | {...extraProps}
17 | />
18 | )}
19 |
20 | );
21 |
22 | export default createFilterRenderer;
23 |
--------------------------------------------------------------------------------
/src/app/Banner.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Banner({children, message = 'Default banner message', color = 'orange', path = '*'}) {
4 |
5 | let defaultLeftMargin = 0;
6 | let showBanner = false;
7 |
8 | if (path === '*') {
9 | showBanner = true;
10 | } else {
11 | if (window.location.pathname === path) {
12 | showBanner = true;
13 | }
14 | }
15 |
16 | if (!showBanner) {
17 | return null;
18 | }
19 |
20 | if (window.location.href.indexOf('development/major') > -1) {
21 | defaultLeftMargin = 200;
22 | }
23 |
24 | return (
25 |
26 | <>
27 | {children}
28 | >
29 |
30 | );
31 | }
32 |
33 | export default Banner;
34 |
--------------------------------------------------------------------------------
/src/styles/bootstrap/mixins/_list-group.scss:
--------------------------------------------------------------------------------
1 | // List Groups
2 |
3 | @mixin list-group-item-variant($state, $background, $color) {
4 | .list-group-item-#{$state} {
5 | color: $color;
6 | background-color: $background;
7 |
8 | // [converter] extracted a&, button& to a.list-group-item-#{$state}, button.list-group-item-#{$state}
9 | }
10 |
11 | a.list-group-item-#{$state},
12 | button.list-group-item-#{$state} {
13 | color: $color;
14 |
15 | .list-group-item-heading {
16 | color: inherit;
17 | }
18 |
19 | &:hover,
20 | &:focus {
21 | color: $color;
22 | background-color: darken($background, 5%);
23 | }
24 | &.active,
25 | &.active:hover,
26 | &.active:focus {
27 | color: #fff;
28 | background-color: $color;
29 | border-color: $color;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/styles/bootstrap/_breadcrumbs.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Breadcrumbs
3 | // --------------------------------------------------
4 |
5 |
6 | .breadcrumb {
7 | padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal;
8 | margin-bottom: $line-height-computed;
9 | list-style: none;
10 | background-color: $breadcrumb-bg;
11 | border-radius: $border-radius-base;
12 |
13 | > li {
14 | display: inline-block;
15 |
16 | + li:before {
17 | // [converter] Workaround for https://github.com/sass/libsass/issues/1115
18 | $nbsp: "\00a0";
19 | content: "#{$breadcrumb-separator}#{$nbsp}"; // Unicode space added since inline-block means non-collapsing white-space
20 | padding: 0 5px;
21 | color: $breadcrumb-color;
22 | }
23 | }
24 |
25 | > .active {
26 | color: $breadcrumb-active-color;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/utilities/lang/LanguageContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const LangContext = React.createContext();
4 |
5 | export default class LanguageProvider extends React.Component {
6 | state = {
7 | language: 'English',
8 | label: 'English',
9 | dropdownOpen: false,
10 | switchLanguage: (newLang, label, dropdownOpen) => {
11 | this.setState({ language: newLang, label, dropdownOpen });
12 | },
13 | }
14 | render() {
15 | return {this.props.children} ;
16 | }
17 | }
18 |
19 | export function withLanguage(Component) {
20 | return function LanguagedComponent(props) {
21 | return (
22 |
23 | {context => }
24 |
25 | );
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/budget/graphql/budgetQueries.js:
--------------------------------------------------------------------------------
1 | import gql from 'graphql-tag';
2 |
3 | export const getSankeyData = gql`
4 | query getSankeyData {
5 | sankeyData @client {
6 | nodes
7 | links
8 | }
9 | }
10 | `;
11 |
12 | export const getBudgetTrees = gql`
13 | query getBudgetTrees {
14 | budgetTrees @client {
15 | expenseTree
16 | revenueTree
17 | expenseTreeForTreemap
18 | revenueTreeForTreemap
19 | }
20 | }
21 | `;
22 |
23 | export const getBudgetSummaryUse = gql`
24 | query getBudgetSummaryUse {
25 | budgetSummaryUse @client {
26 | dataKeys
27 | dataValues
28 | }
29 | }
30 | `;
31 |
32 | export const getBudgetSummaryDept = gql`
33 | query getBudgetSummaryDept {
34 | budgetSummaryDept @client {
35 | dataKeys
36 | dataValues
37 | }
38 | }
39 | `;
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/app/development/volume/DataModal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 |
5 | const DataModal = props => ReactDOM.createPortal(
6 | ,
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 |
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 |
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 | [](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 | {Object.keys(steps).map(stepKey => (
6 |
7 |
14 | {stepKey}?
15 |
16 | {stepKey === 'who' && steps.who &&
17 |
18 | {steps.who.map(actor => (
19 |
20 |
23 | {whoIcons[actor].icon}
24 |
25 |
{whoIcons[actor].label}
26 |
27 | ))}
28 |
29 | }
30 | {stepKey !== 'who' && {steps[stepKey]}
}
31 |
32 | ))}
33 |
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 | {props.label}
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 |
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 |
25 | {this.options.map((option, i) => (
26 | {option.display}
27 | ))}
28 |
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 | {content.back}
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 |
9 |
10 |
11 |
12 |
Dashboards
13 |
City of Asheville, NC
14 |
15 |
16 |
17 |
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 | {content.back}
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 | props.updateAuthModal({
21 | variables: {
22 | open: !props.open,
23 | },
24 | })}
25 | >
26 | ×
27 |
28 |
29 |
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 | {translate('Download', props.language.language)}
35 |
36 | {/*
37 | Email
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 |
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 |
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 |
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 |
35 | {props.children}
36 |
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 |
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 |
{
50 | e.preventDefault();
51 | this.setState({ open: !this.state.open })
52 | }}
53 | >
54 | Filter Record Types
55 |
58 |
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 |
props.onSelect(e, levelIndex)}
45 | value={level.selectedCat || 'null'}
46 | >
47 | All
48 | {orderedKeys.map(key => (
49 |
53 | {key}
54 |
55 | ))}
56 |
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 |
--------------------------------------------------------------------------------