├── src ├── scripts │ ├── .gitignore │ ├── requirements.txt │ ├── csv_debug_tools │ │ ├── add_na_column.py │ │ ├── inspect_csv.py │ │ └── check_column_count.py │ └── updateHfDataset.py ├── pages │ ├── NotFound │ │ └── NotFound.jsx │ ├── Contact │ │ └── Contact.jsx │ ├── Privacy │ │ └── Privacy.jsx │ ├── Home │ │ └── index.jsx │ └── Blog │ │ └── Blog.jsx ├── components │ ├── DateSelector │ │ ├── index.js │ │ ├── options.js │ │ ├── useStyles.js │ │ └── DateRanges.jsx │ ├── common │ │ ├── DatePicker │ │ │ └── index.js │ │ ├── ReactDayPicker │ │ │ ├── index.js │ │ │ └── Weekday.jsx │ │ ├── ChipList │ │ │ ├── index.jsx │ │ │ └── StyledChip.jsx │ │ ├── MultiSelect │ │ │ ├── SelectItem.jsx │ │ │ ├── SelectGroup.jsx │ │ │ └── GroupedMultiSelect.jsx │ │ ├── ArrowToolTip │ │ │ └── index.jsx │ │ ├── GearButton │ │ │ └── index.jsx │ │ ├── SearchBar.jsx │ │ └── ToggleGroup.jsx │ ├── layout │ │ ├── Main │ │ │ ├── Desktop │ │ │ │ ├── TypeSelector │ │ │ │ │ └── isToggle.js │ │ │ │ ├── FilterMenu.test.jsx │ │ │ │ ├── CouncilSelector │ │ │ │ │ ├── SelectedCouncils.jsx │ │ │ │ │ └── CouncilsList.jsx │ │ │ │ ├── Export │ │ │ │ │ ├── useStyles.js │ │ │ │ │ ├── ExportFailure.jsx │ │ │ │ │ ├── ExportDialog.jsx │ │ │ │ │ ├── ExportWarning.jsx │ │ │ │ │ └── ExportDownload.jsx │ │ │ │ └── ShareableLinkCreator.jsx │ │ │ ├── Reports.jsx │ │ │ ├── Research.jsx │ │ │ └── CookieNotice.jsx │ │ ├── ContentBottom │ │ │ └── index.jsx │ │ ├── ContentBody │ │ │ └── index.jsx │ │ ├── Footer │ │ │ ├── SocialMediaLinks.jsx │ │ │ ├── LastUpdated.jsx │ │ │ └── index.jsx │ │ └── TextHeading │ │ │ ├── TextHeadingFAQ.jsx │ │ │ └── index.jsx │ ├── Dashboards │ │ ├── DashboardComparison.jsx │ │ ├── widgets │ │ │ └── TotalByDayOfWeek.jsx │ │ ├── layouts │ │ │ └── QuadLayout.jsx │ │ └── DashboardOverview.jsx │ ├── MaintenanceMode │ │ └── index.jsx │ ├── contact │ │ ├── ContactForm.jsx │ │ ├── ContactImage.jsx │ │ ├── ContactIntro.jsx │ │ └── googleFormScript.js │ └── Loading │ │ ├── FunFactCard.jsx │ │ ├── AcknowledgeModal.jsx │ │ └── LoadingModal.jsx ├── assets │ ├── 311Logo.png │ ├── spinner.png │ ├── cfa-logo.png │ ├── about311hero.png │ ├── contact_bg.png │ ├── publish_icon.png │ ├── screenshot.PNG │ ├── uplift_icon.png │ ├── database_icon.png │ ├── visualize_icon.png │ ├── empower_la_logo.png │ ├── faq │ │ ├── 311-data-maps.png │ │ ├── 311-compare-councils.png │ │ ├── 311-explain-request-types.png │ │ ├── 311-explore-council-data.png │ │ ├── faq-arrow.svg │ │ └── search-outline.svg │ ├── hack_for_la_logo.png │ ├── mapbox-logo-white.png │ ├── mobile_app_icon.png │ ├── about311hero860-min.png │ ├── city_background_header.png │ ├── nav-accessibility-icon.png │ ├── civic_tech_structure_logo.png │ ├── loading dots.svg │ ├── facebook-round.svg │ ├── yellow_tips.svg │ ├── close.svg │ ├── warning-error.svg │ ├── home-15.svg │ ├── download-circle.svg │ ├── round-check-circle.svg │ ├── typcn_export.svg │ ├── address-icon-48.svg │ ├── twitter-round.svg │ ├── logo.svg │ ├── datepicker.svg │ └── aboutmobile.svg ├── theme │ ├── borderRadius.js │ ├── gaps.js │ ├── fonts.js │ ├── styles.js │ ├── layout.js │ ├── colors.js │ ├── typography.js │ └── theme.js ├── utils │ ├── not.js │ ├── test-setup.js │ ├── toTitleCase.js │ ├── utils.test.js │ ├── Mixpanel.js │ ├── test-utils.jsx │ ├── duckDbHelpers.js │ └── checkEnv.js ├── redux │ ├── rootSaga.js │ ├── rootReducer.js │ ├── reducers │ │ ├── analytics.js │ │ ├── mapFilters.js │ │ └── metadata.js │ ├── store.js │ ├── sagas │ │ └── metadata.js │ └── tempTypesApi.js ├── features │ └── Map │ │ ├── controls │ │ ├── MapRegion.jsx │ │ ├── MapMeta.jsx │ │ ├── RequestsBarChart.jsx │ │ └── RequestsDonut.jsx │ │ ├── constants.js │ │ ├── districts.js │ │ ├── zoomTooltip.jsx │ │ ├── mapColors.js │ │ ├── LocationDetail.jsx │ │ └── geoUtils.js ├── hooks │ ├── useOutsideClick.js │ └── useContentful.js ├── settings │ └── index.js ├── main.jsx ├── templates │ └── contact-us.html ├── routes │ └── Routes.jsx └── App.jsx ├── favicon.png ├── public ├── favicon.png └── social-media-card-image.png ├── .eslintignore ├── backend ├── DbContext.jsx ├── metadata.js ├── regions.js ├── facts.js └── agencies.js ├── babel.config.json ├── .example.env ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── post-an-open-role.md │ ├── blank-issue.md │ ├── blank_epic.md │ ├── emergent-request.md │ ├── blank-dev-issue.md │ ├── blank-design-issue.md │ └── blank-research-plan.md └── workflows │ ├── dash-reports-deploy.yml │ ├── create_prefect_dev.yml │ ├── Continuous_Deployment_Backend_Prod.yml │ ├── hfUpdate.yml │ ├── build_test_backend.yml │ ├── Continuous_Integration_Frontend.yml │ ├── main.yml │ ├── Continuous_Deployment_Frontend_Prod.yml │ ├── create_release_dev.yml │ ├── Continuous_Deployment_Frontend_Dev.yml │ └── create_release_dev_v1.yml ├── pre_commit_lint.sh ├── .gitignore ├── vite.config.js ├── .eslintrc.js ├── package.json └── index.html /src/scripts/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *.csv 3 | /venv 4 | -------------------------------------------------------------------------------- /src/pages/NotFound/NotFound.jsx: -------------------------------------------------------------------------------- 1 | //TODO: Create a 404 page -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/favicon.png -------------------------------------------------------------------------------- /src/components/DateSelector/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './DateSelector'; 2 | -------------------------------------------------------------------------------- /src/components/common/DatePicker/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './DatePicker'; 2 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/public/favicon.png -------------------------------------------------------------------------------- /src/components/common/ReactDayPicker/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './ReactDayPicker'; 2 | -------------------------------------------------------------------------------- /src/assets/311Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/311Logo.png -------------------------------------------------------------------------------- /src/assets/spinner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/spinner.png -------------------------------------------------------------------------------- /src/assets/cfa-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/cfa-logo.png -------------------------------------------------------------------------------- /src/assets/about311hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/about311hero.png -------------------------------------------------------------------------------- /src/assets/contact_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/contact_bg.png -------------------------------------------------------------------------------- /src/assets/publish_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/publish_icon.png -------------------------------------------------------------------------------- /src/assets/screenshot.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/screenshot.PNG -------------------------------------------------------------------------------- /src/assets/uplift_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/uplift_icon.png -------------------------------------------------------------------------------- /src/assets/database_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/database_icon.png -------------------------------------------------------------------------------- /src/assets/visualize_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/visualize_icon.png -------------------------------------------------------------------------------- /src/assets/empower_la_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/empower_la_logo.png -------------------------------------------------------------------------------- /src/assets/faq/311-data-maps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/faq/311-data-maps.png -------------------------------------------------------------------------------- /src/assets/hack_for_la_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/hack_for_la_logo.png -------------------------------------------------------------------------------- /src/assets/mapbox-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/mapbox-logo-white.png -------------------------------------------------------------------------------- /src/assets/mobile_app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/mobile_app_icon.png -------------------------------------------------------------------------------- /src/theme/borderRadius.js: -------------------------------------------------------------------------------- 1 | export default { 2 | sm: 5, 3 | md: 10, 4 | lg: 50, 5 | round: '50%', 6 | }; 7 | -------------------------------------------------------------------------------- /src/theme/gaps.js: -------------------------------------------------------------------------------- 1 | export default { 2 | xs: 5, 3 | sm: 10, 4 | md: 15, 5 | lg: 20, 6 | xl: 30, 7 | }; 8 | -------------------------------------------------------------------------------- /public/social-media-card-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/public/social-media-card-image.png -------------------------------------------------------------------------------- /src/assets/about311hero860-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/about311hero860-min.png -------------------------------------------------------------------------------- /src/assets/city_background_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/city_background_header.png -------------------------------------------------------------------------------- /src/assets/nav-accessibility-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/nav-accessibility-icon.png -------------------------------------------------------------------------------- /src/assets/faq/311-compare-councils.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/faq/311-compare-councils.png -------------------------------------------------------------------------------- /src/assets/civic_tech_structure_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/civic_tech_structure_logo.png -------------------------------------------------------------------------------- /src/assets/faq/311-explain-request-types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/faq/311-explain-request-types.png -------------------------------------------------------------------------------- /src/assets/faq/311-explore-council-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackforla/311-data/HEAD/src/assets/faq/311-explore-council-data.png -------------------------------------------------------------------------------- /src/utils/not.js: -------------------------------------------------------------------------------- 1 | export default function not(a, b, key) { 2 | return a.filter(itemA => !b.some(itemB => itemA[key] === itemB[key])); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/test-setup.js: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | vi.mock('@mui/styles/makeStyles', () => ({ 4 | default: vi.fn(() => vi.fn(() => ({}))) 5 | })); 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.test.js* 3 | **/v1/* 4 | **/dist/* 5 | **/node_modules/ 6 | **/public/* 7 | **/Temp/* 8 | *\ copy.* 9 | **/data/* 10 | **/archive/* 11 | -------------------------------------------------------------------------------- /src/utils/toTitleCase.js: -------------------------------------------------------------------------------- 1 | export default function toTitleCase(str) { 2 | return str.replace( 3 | /\w\S*/g, 4 | txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(), 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/faq/faq-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/DbContext.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DbContext = React.createContext({ 4 | db: null, 5 | conn: null, 6 | worker: null, 7 | tableNameByYear: '', 8 | startTime: null, 9 | }); 10 | 11 | export default DbContext; 12 | -------------------------------------------------------------------------------- /src/assets/loading dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/redux/rootSaga.js: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | 3 | import metadata from './sagas/metadata'; 4 | import data from './sagas/data'; 5 | import analytics from './sagas/analytics'; 6 | 7 | export default function* rootSaga() { 8 | yield all([ 9 | metadata(), 10 | data(), 11 | analytics(), 12 | ]); 13 | } 14 | -------------------------------------------------------------------------------- /src/scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2023.5.7 2 | charset-normalizer==3.1.0 3 | duckdb==0.8.1 4 | filelock==3.12.2 5 | fsspec==2023.6.0 6 | huggingface-hub==0.15.1 7 | idna==3.4 8 | packaging==23.1 9 | python-dotenv==1.0.0 10 | PyYAML==6.0 11 | requests==2.31.0 12 | tdqm==0.0.1 13 | tqdm==4.65.0 14 | typing_extensions==4.6.3 15 | urllib3==2.0.3 16 | -------------------------------------------------------------------------------- /src/theme/fonts.js: -------------------------------------------------------------------------------- 1 | // Define standard font constants. 2 | export default { 3 | family: { 4 | oswald: 'Oswald, sans-serif', 5 | roboto: 'Roboto, sans-serif', 6 | }, 7 | weight: { 8 | medium: 500, 9 | regular: 400, 10 | semiBold: 600, 11 | bold: 700, 12 | }, 13 | size: { 14 | jumbo: '46px', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": "usage", 7 | "corejs": 3, 8 | "debug": false, 9 | "targets": "> 1%, last 2 versions, not dead" 10 | } 11 | ], 12 | "@babel/preset-react" 13 | ], 14 | "plugins": [ 15 | "@babel/plugin-proposal-class-properties" 16 | ] 17 | } -------------------------------------------------------------------------------- /src/features/Map/controls/MapRegion.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import React from 'react'; 4 | import PropTypes from 'proptypes'; 5 | 6 | const MapRegion = ({ regionName }) => { 7 | if (!regionName) 8 | return null; 9 | 10 | return ( 11 |
12 | { regionName } 13 |
14 | ); 15 | }; 16 | 17 | export default MapRegion; 18 | -------------------------------------------------------------------------------- /src/redux/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import metadata from './reducers/metadata'; 3 | import data from './reducers/data'; 4 | import filters from './reducers/filters'; 5 | import ui from './reducers/ui'; 6 | import mapFilters from './reducers/mapFilters'; 7 | 8 | export default combineReducers({ 9 | metadata, 10 | data, 11 | filters, 12 | ui, 13 | mapFilters, 14 | }); 15 | -------------------------------------------------------------------------------- /src/redux/reducers/analytics.js: -------------------------------------------------------------------------------- 1 | export const types = { 2 | TRACK_MAP_EXPORT: 'TRACK_MAP_EXPORT', 3 | TRACK_CHART_EXPORT: 'TRACK_CHART_EXPORT', 4 | }; 5 | 6 | export const trackMapExport = () => ({ 7 | type: types.TRACK_MAP_EXPORT, 8 | }); 9 | 10 | export const trackChartExport = ({ pageArea, fileType, path }) => ({ 11 | type: types.TRACK_CHART_EXPORT, 12 | payload: { pageArea, fileType, path }, 13 | }); 14 | -------------------------------------------------------------------------------- /.example.env: -------------------------------------------------------------------------------- 1 | HUGGINGFACE_LOGIN_TOKEN=REDACTED 2 | MAPBOX_SATELLITE_URL=https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v11/tiles/{z}/{x}/{y} 3 | MAPBOX_STREETS_URL=https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y} 4 | VITE_ENV=DEV 5 | VITE_CONTACT_FORM=REDACTED 6 | VITE_CONTENTFUL_SPACE=REDACTED 7 | VITE_CONTENTFUL_TOKEN=REDACTED 8 | VITE_MAPBOX_TOKEN=REDACTED 9 | VITE_DATA_SOURCE=HF # Options: HF, SOCRATA -------------------------------------------------------------------------------- /backend/metadata.js: -------------------------------------------------------------------------------- 1 | const metadata = { 2 | currentTimeUTC: '2023-04-14T02:31:56.958349', 3 | lastPulledUTC: '2023-04-13T08:22:01.580116', 4 | currentTimeLocal: '2023-04-13T19:31:56.958365-07:00', 5 | lastPulledLocal: '2023-04-13T01:22:01.580116-07:00', 6 | stage: 'Development', 7 | version: 'dev', 8 | gitSha: '8e875eb6c9a23ae291dcc98fe90b8b13d881a7f8', 9 | process: 13120, 10 | }; 11 | 12 | export default metadata; 13 | -------------------------------------------------------------------------------- /src/assets/faq/search-outline.svg: -------------------------------------------------------------------------------- 1 | ionicons-v5-f -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes #{issue number here} 2 | 3 | - [ ] Up to date with `main` branch 4 | - [ ] Branch name follows [guidelines](https://github.com/hackforla/311-data/blob/master/GETTING_STARTED.md#feature-branching) 5 | - [ ] All PR Status checks are successful 6 | - [ ] Peer reviewed and approved 7 | 8 | Any questions? See the [getting started guide](https://github.com/hackforla/311-data/blob/master/GETTING_STARTED.md) 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/post-an-open-role.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Post an open role 3 | about: Recruit volunteers for specific open roles template 4 | title: '311Data: Open Role for: [Replace with NAME OF ROLE]' 5 | labels: 'feature: recruiting, Role: Missing, size: 0.25pt' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | [INSERT DRAFT FROM THE Recruit volunteers for team open roles issue] 13 | -------------------------------------------------------------------------------- /pre_commit_lint.sh: -------------------------------------------------------------------------------- 1 | for file in $(git diff --cached --name-only | grep -E '\.(js|jsx)$') 2 | do 3 | git show ":$file" | node_modules/.bin/eslint --fix-dry-run --stdin --stdin-filename "$file" # we only want to lint the staged changes, not any un-staged changes 4 | if [ $? -ne 0 ]; then 5 | echo "ESLint failed on staged file '$file'. Please check your code and try again. You can run ESLint manually via npm run eslint." 6 | exit 1 # exit with failure status 7 | fi 8 | done -------------------------------------------------------------------------------- /src/assets/facebook-round.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /src/components/common/ReactDayPicker/Weekday.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { WeekdayElementProps } from 'react-day-picker'; 3 | 4 | function Weekday({ 5 | weekday, className, localeUtils, locale, 6 | }) { 7 | const weekdayName = localeUtils.formatWeekdayLong(weekday, locale); 8 | return ( 9 |
10 | {weekdayName.slice(0, 3)} 11 |
12 | ); 13 | } 14 | 15 | Weekday.propTypes = WeekdayElementProps; 16 | 17 | export default Weekday; 18 | -------------------------------------------------------------------------------- /src/utils/utils.test.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { isEmpty } from '@utils'; 3 | 4 | describe('isEmpty', () => { 5 | test('nullish value', () => { 6 | expect(isEmpty(undefined)).toBe(true); 7 | }); 8 | 9 | test('empty object', () => { 10 | expect(isEmpty({})).toBe(true); 11 | }); 12 | 13 | test('empty array', () => { 14 | expect(isEmpty([])).toBe(true); 15 | }); 16 | 17 | test('non-empty array', () => { 18 | expect(isEmpty([1, 2])).toBe(false); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/hooks/useOutsideClick.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | const useOutsideClick = (ref, callback) => { 4 | useEffect(() => { 5 | const handleOutsideClick = e => { 6 | if (ref.current && !ref.current.contains(e.target)) { 7 | callback(); 8 | } 9 | }; 10 | 11 | document.addEventListener('click', handleOutsideClick); 12 | return () => { 13 | document.removeEventListener('click', handleOutsideClick); 14 | }; 15 | }, [ref, callback]); 16 | }; 17 | 18 | export default useOutsideClick; 19 | -------------------------------------------------------------------------------- /src/theme/styles.js: -------------------------------------------------------------------------------- 1 | import makeStyles from '@mui/styles/makeStyles'; 2 | import createStyles from '@mui/styles/createStyles'; 3 | import fonts from '@theme/fonts'; 4 | 5 | // Define common styles as makeStyles hook. 6 | const sharedStyles = makeStyles(theme => createStyles({ 7 | // Desktop Menu 8 | headerTitle: { 9 | ...theme.typography.h5, 10 | fontFamily: fonts.family.oswald, 11 | fontWeight: fonts.weight.semiBold, 12 | letterSpacing: '2px', 13 | color: theme.palette.text.cyan, 14 | }, 15 | })); 16 | 17 | export default sharedStyles; 18 | -------------------------------------------------------------------------------- /src/settings/index.js: -------------------------------------------------------------------------------- 1 | const settings = { 2 | map: { 3 | debounce: { 4 | duration: 100, // milliseconds 5 | options: { 6 | leading: true, // calls function immediately when invoked (React-friendly) 7 | trailing: false, // calls function after last input event fired (Not React-friendly) 8 | }, 9 | }, 10 | eventName: { 11 | // to keep event handler names consistent throughout our codebase 12 | reset: 'reset', 13 | }, 14 | }, 15 | selectItem: { 16 | maxLen: 28, 17 | }, 18 | }; 19 | 20 | export default settings; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/blank-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Blank Issue 3 | about: Describe this issue's purpose here 4 | title: '' 5 | labels: 'Complexity: Missing, draft, Feature: Missing, Milestone: Missing, Role: Missing, 6 | size: Missing' 7 | assignees: '' 8 | 9 | --- 10 | 11 | ### Overview 12 | We need to do X for Y reason. 13 | 14 | ### Action Items 15 | 16 | - [ ] Action Item 1 17 | - [ ] Action Item 2 18 | - [ ] Action Item 99 19 | 20 | ### Resources/Instructions 21 | REPLACE THIS TEXT -If there is a website which has documentation that helps with this issue provide the link(s) here. 22 | -------------------------------------------------------------------------------- /src/components/layout/Main/Desktop/TypeSelector/isToggle.js: -------------------------------------------------------------------------------- 1 | import { 2 | useEffect, useRef, useCallback, useState, 3 | } from 'react'; 4 | 5 | const useToggle = initialState => { 6 | const [isToggled, setIsToggled] = useState(initialState); 7 | const isToggledRef = useRef(isToggled); 8 | const toggle = useCallback( 9 | () => setIsToggled(!isToggledRef.current), 10 | [isToggledRef, setIsToggled], 11 | ); 12 | 13 | useEffect(() => { 14 | isToggledRef.current = isToggled; 15 | }, [isToggled]); 16 | return [isToggled, toggle]; 17 | }; 18 | 19 | export default useToggle; 20 | -------------------------------------------------------------------------------- /src/components/DateSelector/options.js: -------------------------------------------------------------------------------- 1 | import { DateUtils } from 'react-day-picker'; 2 | 3 | const today = new Date(); 4 | 5 | const oneMonthBack = DateUtils.addMonths(today, -1); 6 | const threeMonthsBack = DateUtils.addMonths(today, -3); 7 | const oneWeekBack = new Date(new Date().setDate(today.getDate() - 7)); 8 | 9 | const options = [ 10 | { 11 | text: 'Last Week', 12 | dates: [oneWeekBack, today], 13 | }, 14 | { 15 | text: 'Last Month', 16 | dates: [oneMonthBack, today], 17 | }, 18 | { 19 | text: 'Last 3 Months', 20 | dates: [threeMonthsBack, today], 21 | }, 22 | ]; 23 | 24 | export default options; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # secrets 2 | .env 3 | 4 | # dependencies 5 | /node_modules/ 6 | node_modules_non_es5 7 | 8 | # production build 9 | dist/ 10 | 11 | # testing 12 | __snapshots__ 13 | /coverage/ 14 | 15 | # data 16 | /data/*.py 17 | /data/*-old.* 18 | /data/*.csv 19 | /data/csv/* 20 | /data/*.geojson 21 | 22 | # misc 23 | tags/ 24 | Temp/ 25 | *-new.* 26 | *.old 27 | *\ copy.* 28 | stats.json 29 | docker/ 30 | venv/ 31 | .vscode 32 | .editorconfig 33 | .prettierignore 34 | 35 | # socrata experimental files 36 | *-socrata* 37 | 38 | # duckDb experimental files 39 | *-duckDb* 40 | 41 | # archive 42 | /archive/aws/API\ Responses/* 43 | 44 | .DS_Store 45 | 46 | -------------------------------------------------------------------------------- /src/assets/yellow_tips.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/features/Map/constants.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { boundingBox } from './geoUtils'; 4 | import { ncBoundaries } from './districts'; 5 | 6 | export const INITIAL_BOUNDS = boundingBox(ncBoundaries); 7 | 8 | export const INITIAL_LOCATION = { 9 | location: 'All of Los Angeles', 10 | }; 11 | 12 | export const GEO_FILTER_TYPES = { 13 | address: 'Address', 14 | nc: 'District', 15 | }; 16 | 17 | export const MAP_STYLES = { 18 | dark: 'mapbox://styles/mapbox/dark-v10', 19 | light: 'mapbox://styles/mapbox/light-v11', 20 | streets: 'mapbox://styles/mapbox/streets-v11', 21 | satellite: 'mapbox://styles/mapbox/satellite-streets-v11', 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Dashboards/DashboardComparison.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import ContentBody from '@components/common/ContentBody'; 4 | // import ddbh from '@utils/duckDbHelpers.js'; 5 | // import DbContext from '@db/DbContext'; 6 | 7 | function DashboardComparison() { 8 | const isMapLoading = useSelector(state => state.data.isMapLoading); 9 | 10 | if (isMapLoading) return null; 11 | 12 | // const { conn } = useContext(DbContext); 13 | return ( 14 | 15 |

Welcome to the future of Dashboard Comparison

16 |
17 | ); 18 | } 19 | 20 | export default DashboardComparison; 21 | -------------------------------------------------------------------------------- /src/assets/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/dash-reports-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Dash Reports Image CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | paths: 8 | - 'server/dash/**' 9 | 10 | jobs: 11 | build: 12 | name: Create Docker Image 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Build and Push Image to Docker Hub 18 | uses: docker/build-push-action@v1 19 | with: 20 | username: ${{ secrets.DOCKER_USERNAME }} 21 | password: ${{ secrets.DOCKER_PASSWORD }} 22 | path: server/dash 23 | repository: la311data/dash-poc 24 | tag_with_ref: true 25 | tag_with_sha: true 26 | -------------------------------------------------------------------------------- /src/assets/warning-error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/create_prefect_dev.yml: -------------------------------------------------------------------------------- 1 | name: Create Prefect Image (DEV) 2 | on: 3 | push: 4 | branches: 5 | - dev 6 | paths: 7 | - 'server/prefect/**' 8 | jobs: 9 | build: 10 | name: Create Docker Image 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | - name: Build and Push Image to Docker Hub 16 | uses: docker/build-push-action@v1 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | path: server/prefect 21 | repository: la311data/311_data_prefect 22 | tag_with_ref: true 23 | tag_with_sha: true 24 | -------------------------------------------------------------------------------- /src/components/Dashboards/widgets/TotalByDayOfWeek.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; //! Cleanup - delete 2 | import PropTypes from 'proptypes'; 3 | 4 | function TotalByDayOfWeek({ data }) { 5 | return ( 6 | <> 7 |
Total Requests by Day of The Week
8 | { 9 | // Observable code goes here 10 | // https://observablehq.com/d/8236de092b1f9523#cell-237 11 | } 12 | 17 | 18 | ); 19 | } 20 | 21 | TotalByDayOfWeek.propTypes = { 22 | data: PropTypes.arrayOf(PropTypes.shape({})).isRequired, 23 | }; 24 | 25 | export default TotalByDayOfWeek; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/blank_epic.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Blank Epic 3 | about: Template for PMs to create Epics to track batches of features in a milestone 4 | title: '' 5 | labels: 'Complexity: Small, draft, Epic, Feature: Missing, Milestone: Missing, Role: 6 | Missing, size: 0.25pt' 7 | assignees: '' 8 | 9 | --- 10 | 11 | ### Overview 12 | 13 | This epic tracks all issues related to this (pick one or more: feature/page/milestone) 14 | 15 | ### Resources/Instructions 16 | 17 |
Screenshot before proposed changes 18 |

19 | [insert screenshot here] 20 |

21 |
22 | 23 |
Screenshot after proposed changes 24 |

25 | [insert screenshot here] 26 |

27 |
28 | -------------------------------------------------------------------------------- /src/assets/home-15.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/Continuous_Deployment_Backend_Prod.yml: -------------------------------------------------------------------------------- 1 | name: Deploy_Backend_Prod 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'server/**' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: run deploy script on AWS prod 15 | uses: appleboy/ssh-action@master 16 | with: 17 | username: ec2-user 18 | host: ${{ secrets.AWS_SSH_HOST_PROD }} 19 | key: ${{ secrets.AWS_SSH_PEM_KEY }} 20 | script: | 21 | set -e 22 | cd 311-data/server 23 | sudo git pull 24 | echo GITHUB_SHA=${{ github.sha }} >> .env 25 | docker-compose build api 26 | docker-compose up --no-deps -d api 27 | -------------------------------------------------------------------------------- /src/utils/Mixpanel.js: -------------------------------------------------------------------------------- 1 | import mixpanel from 'mixpanel-browser'; 2 | 3 | const token = import.meta.env.PROD 4 | ? import.meta.env.MIXPANEL_TOKEN_PROD 5 | : import.meta.env.MIXPANEL_TOKEN_DEV; 6 | 7 | // Set MIXPANEL_ENABLED env variable to: 8 | // 1 or greater to enable Mixpanel logging 9 | // 0 to disable Mixpanel logging 10 | const mixpanelEnabled = import.meta.env.MIXPANEL_ENABLED > 0; 11 | 12 | if (mixpanelEnabled) { 13 | mixpanel.init(token); 14 | } 15 | 16 | const Mixpanel = { 17 | track: (name, props) => { 18 | if (mixpanelEnabled) { 19 | mixpanel.track(name, props); 20 | } 21 | }, 22 | time_event: name => { 23 | if (mixpanelEnabled) { 24 | mixpanel.time_event(name); 25 | } 26 | }, 27 | }; 28 | 29 | export default Mixpanel; 30 | -------------------------------------------------------------------------------- /src/features/Map/controls/MapMeta.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | import PropTypes from 'proptypes'; 5 | 6 | const MapMeta = ({ map }) => { 7 | const [zoom, setZoom] = useState(map.getZoom()); 8 | 9 | useEffect(() => { 10 | const onZoomEnd = () => setZoom(map.getZoom()); 11 | map.on('zoomend', onZoomEnd); 12 | return () => map.off('zoomend', onZoomEnd); 13 | }, []); 14 | 15 | return ( 16 |
25 | { zoom.toFixed(2) } 26 |
27 | ); 28 | }; 29 | 30 | export default MapMeta; 31 | -------------------------------------------------------------------------------- /src/theme/layout.js: -------------------------------------------------------------------------------- 1 | import makeStyles from '@mui/styles/makeStyles'; 2 | import createStyles from '@mui/styles/createStyles'; 3 | 4 | // Define standard layout spacing and export as makeStyles hook. 5 | const sharedLayout = makeStyles(theme => createStyles({ 6 | // Top margins 7 | marginTopLarge: { 8 | marginTop: theme.spacing(5), 9 | }, 10 | marginTopMedium: { 11 | marginTop: theme.spacing(3), 12 | }, 13 | marginTopSmall: { 14 | marginTop: theme.spacing(1), 15 | }, 16 | 17 | // Bottom margins 18 | marginBottomLarge: { 19 | marginBottom: theme.spacing(5), 20 | }, 21 | marginBottomMedium: { 22 | marginBottom: theme.spacing(3), 23 | }, 24 | marginBottomSmall: { 25 | marginBottom: theme.spacing(1), 26 | }, 27 | })); 28 | 29 | export default sharedLayout; 30 | -------------------------------------------------------------------------------- /src/components/MaintenanceMode/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import React from 'react'; //! Cleanup - delete 4 | 5 | const MaintenanceMode = () => ( 6 |
7 |
8 | 311 9 | DATA 10 |
11 |
12 | 13 | Hack for LA's 311 data analysis is down temporarily while we rebuild. 14 | If you are looking to place a 311 ticket, please visit Los Angeles's 311 system: 15 | {' '} 16 | 17 | https://myla311.lacity.org/ 18 |
19 |
20 |
21 |
22 | ); 23 | 24 | export default MaintenanceMode; 25 | -------------------------------------------------------------------------------- /src/utils/test-utils.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | import { Provider } from 'react-redux' 4 | import { setupStore } from '../redux/store' 5 | 6 | // https://redux.js.org/usage/writing-tests#setting-up-a-reusable-test-render-function 7 | export function renderWithProviders(ui, extendedRenderOptions = {}) { 8 | const { 9 | preloadedState = {}, 10 | // Automatically create a store instance if no store was passed in 11 | store = setupStore(preloadedState), 12 | ...renderOptions 13 | } = extendedRenderOptions 14 | 15 | const Wrapper = ({ children }) => ( 16 | {children} 17 | ) 18 | 19 | // Return an object with the store and all of RTL's query functions 20 | return { 21 | store, 22 | ...render(ui, { wrapper: Wrapper, ...renderOptions }) 23 | } 24 | } -------------------------------------------------------------------------------- /.github/workflows/hfUpdate.yml: -------------------------------------------------------------------------------- 1 | name: Update Huggingface Dataset 2 | 3 | on: 4 | schedule: # once daily at 3:05 pm PST (23:05 UTC) 5 | - cron: "5 23 * * *" 6 | 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | defaults: 11 | run: 12 | working-directory: scripts/ 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up Python environment 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: "3.11.x" 21 | 22 | - name: Install dependencies 23 | run: pip install -r requirements.txt 24 | 25 | - name: Setup environment 26 | run: | 27 | echo "HUGGINGFACE_LOGIN_TOKEN=${{ secrets.HUGGINGFACE_LOGIN_TOKEN }}" > .env 28 | echo "VITE_ENV=${{ secrets.VITE_ENV }}" >> .env 29 | - name: Run script 30 | run: python updateHfDataset.py 31 | -------------------------------------------------------------------------------- /src/components/contact/ContactForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import makeStyles from '@mui/styles/makeStyles'; 3 | 4 | const useStyles = makeStyles(({ 5 | formContainer: { 6 | width: '100%', 7 | height: 'auto', 8 | display: 'flex', 9 | justifyContent: 'center', 10 | alignItems: 'center', 11 | }, 12 | iframe: { 13 | border: 'none', 14 | width: '100%', 15 | height: '1200px', 16 | minHeight: '856px', 17 | }, 18 | })); 19 | 20 | function ContactForm() { 21 | const classes = useStyles(); 22 | 23 | return ( 24 |
25 | {/* Embed the Google Form */} 26 | 33 |
34 | ); 35 | } 36 | 37 | export default ContactForm; 38 | -------------------------------------------------------------------------------- /src/components/layout/ContentBottom/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Grid from '@mui/material/Grid'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | 5 | const useStyles = makeStyles(theme => ({ 6 | bottomSpacing: { 7 | height: theme.footer.height, 8 | }, 9 | })); 10 | 11 | // ContentBottom is used to provide the necessary amount of bottom margin on 12 | // all content pages to prevent the fixed footer from covering the 13 | // bottom of the content pages. This component is utilized at the bottom of the 14 | // component page of Routes.jsx 15 | 16 | function ContentBottom() { 17 | const classes = useStyles(); 18 | return ( 19 | 20 | {/* an empty grid container with footer height to prevent 21 | * fixed positioned footer from obscuring submit button */} 22 | 23 | ); 24 | } 25 | 26 | export default ContentBottom; 27 | -------------------------------------------------------------------------------- /src/components/Dashboards/layouts/QuadLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; //! Cleanup - delete 2 | import PropTypes from 'prop-types'; 3 | import { Grid } from '@mui/material'; 4 | 5 | function QuadLayout({ 6 | quadrant1, quadrant2, quadrant3, quadrant4, 7 | }) { 8 | return ( 9 | 10 | 11 | {quadrant1} 12 | 13 | 14 | {quadrant2} 15 | 16 | 17 | {quadrant3} 18 | 19 | 20 | {quadrant4} 21 | 22 | 23 | ); 24 | } 25 | 26 | QuadLayout.propTypes = { 27 | quadrant1: PropTypes.element.isRequired, 28 | quadrant2: PropTypes.element.isRequired, 29 | quadrant3: PropTypes.element.isRequired, 30 | quadrant4: PropTypes.element.isRequired, 31 | }; 32 | 33 | export default QuadLayout; 34 | -------------------------------------------------------------------------------- /src/components/layout/Main/Desktop/FilterMenu.test.jsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { fireEvent, screen } from "@testing-library/react"; 3 | import { renderWithProviders } from "@utils/test-utils"; 4 | import FilterMenu from "./FilterMenu"; 5 | 6 | describe('FilterMenu', () => { 7 | it('selects/deselects all request types', () => { 8 | renderWithProviders(); 9 | 10 | const requestTypes = screen.getAllByRole('checkbox'); 11 | const allCheckbox = screen.getByLabelText('Select/Deselect All'); 12 | 13 | requestTypes.forEach(checkbox => { 14 | expect(checkbox.checked).toEqual(true); 15 | }); 16 | 17 | fireEvent.click(allCheckbox); 18 | requestTypes.forEach(checkbox => { 19 | expect(checkbox.checked).toEqual(false); 20 | }); 21 | 22 | fireEvent.click(allCheckbox); 23 | requestTypes.forEach(checkbox => { 24 | expect(checkbox.checked).toEqual(true); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from 'redux'; 2 | import createSagaMiddleware from 'redux-saga'; 3 | import { createLogger } from 'redux-logger'; 4 | 5 | import { composeWithDevToolsDevelopmentOnly } from '@redux-devtools/extension'; 6 | import rootReducer from './rootReducer'; 7 | import rootSaga from './rootSaga'; 8 | 9 | const sagaMiddleware = createSagaMiddleware(); 10 | const middlewares = [sagaMiddleware]; 11 | 12 | if (import.meta.env.DEV) { 13 | const logger = createLogger({ 14 | collapsed: (getState, action, logEntry) => !logEntry.error, 15 | }); 16 | middlewares.push(logger); 17 | } 18 | 19 | export function setupStore(preloadedState = {}) { 20 | return createStore( 21 | rootReducer, 22 | preloadedState, 23 | composeWithDevToolsDevelopmentOnly( 24 | applyMiddleware(...middlewares), 25 | ) 26 | ); 27 | } 28 | 29 | const store = setupStore(); 30 | 31 | sagaMiddleware.run(rootSaga); 32 | 33 | export default store; 34 | -------------------------------------------------------------------------------- /src/theme/colors.js: -------------------------------------------------------------------------------- 1 | // Define standard color constants. 2 | export default { 3 | primaryDark: '#192730', 4 | primaryDarkMain: '#29404F', 5 | primaryFocus: '#FFB104', 6 | primaryLight: '#29404F', 7 | secondaryDark: '#0F181F', 8 | secondaryFocus: '#87C8BC', 9 | selectedPrimary: 'rgba(129, 123, 123, 0.3)', 10 | textDark: '#C4C4C4', 11 | textPrimaryDark: '#0F181F', 12 | textPrimaryLight: '#29404F', 13 | textSecondaryDark: '#A8A8A8', 14 | textSecondaryLight: '#ECECEC', 15 | textFocus: '#87C8BC', 16 | formInput: '#29404F1A', 17 | 18 | requestTypes: { 19 | animalRemains: '#3CB4B2', 20 | bulkyItems: '#DF9286', 21 | graffiti: '#C5E406', 22 | eWaste: '#FF7A93', 23 | homeless: '#15BC76', 24 | illegalDumping: '#A49FD1', 25 | metalHouseholdAppliance: '#C056C8', 26 | multiStreetlight: '#EDAD08', 27 | singleStreetlight: '#79B74E', 28 | waterWaste: '#54ABDE', 29 | feedback: '#F86747', 30 | other: '#F58505', 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/hooks/useContentful.js: -------------------------------------------------------------------------------- 1 | /* eslint no-shadow: ["error", { "allow": ["data", "errors"] }] */ 2 | import React from 'react'; 3 | 4 | const url = `https://graphql.contentful.com/content/v1/spaces/${import.meta.env.VITE_CONTENTFUL_SPACE}`; 5 | 6 | const useContentful = query => { 7 | const [data, setData] = React.useState(null); 8 | const [errors, setErrors] = React.useState(null); 9 | 10 | React.useEffect(() => { 11 | fetch(url, { 12 | method: 'POST', 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | Authorization: `Bearer ${import.meta.env.VITE_CONTENTFUL_TOKEN}`, 16 | }, 17 | body: JSON.stringify({ query }), 18 | }) 19 | .then(response => response.json()) 20 | .then(({ data, errors }) => { 21 | if (errors) setErrors(errors); 22 | if (data) setData(data); 23 | }) 24 | .catch(error => setErrors([error])); 25 | }, [query]); 26 | 27 | return { data, errors }; 28 | }; 29 | 30 | export default useContentful; 31 | -------------------------------------------------------------------------------- /src/components/common/ChipList/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import Box from '@mui/material/Box'; 5 | 6 | const useStyles = makeStyles({ 7 | root: { 8 | fontFamily: 'Roboto', 9 | display: 'flex', 10 | flexWrap: 'wrap', 11 | listStyle: 'none', 12 | margin: 0, 13 | padding: 0, 14 | }, 15 | itemWrapper: { 16 | padding: 2, 17 | }, 18 | }); 19 | 20 | function ChipList({ 21 | children, 22 | }) { 23 | const classes = useStyles(); 24 | 25 | return ( 26 | 27 | { 28 | React.Children.map(children, child => ( 29 |
  • {child}
  • 30 | )) 31 | } 32 |
    33 | ); 34 | } 35 | 36 | export default ChipList; 37 | export { default as StyledChip } from './StyledChip'; 38 | 39 | ChipList.propTypes = { 40 | children: PropTypes.node.isRequired, 41 | }; 42 | -------------------------------------------------------------------------------- /src/assets/download-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/pages/Contact/Contact.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ToastContainer } from 'react-toastify'; 3 | import TextHeading from '@components/layout/TextHeading'; 4 | import ContentBody from '@components/layout/ContentBody'; 5 | import ContactIntro from '@components/contact/ContactIntro'; 6 | import ContactForm from '@components/contact/ContactForm'; 7 | 8 | import 'react-toastify/dist/ReactToastify.css'; 9 | 10 | function Contact() { 11 | return ( 12 | <> 13 |
    14 | 25 |
    26 |
    27 | Contact Us 28 | 29 | 30 | 31 | 32 |
    33 | 34 | ); 35 | } 36 | 37 | export default Contact; 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/emergent-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Emergent Request 3 | about: When you discover something in your issue that is out of scope and it needs 4 | a new issue or discussion. 5 | title: 'ER: (title, required)' 6 | labels: 'Complexity: Small, Discussion, Emergent Request, Feature: Missing, Question, 7 | Role: Missing, size: 0.25pt' 8 | assignees: '' 9 | 10 | --- 11 | 12 | ### Emergent Request - Problem Description 13 | - (short-answer, required) 14 | 15 | ### Relevant Issue(s) 16 | - (issue-hyperlink, required) 17 | 18 | ### Date discovered 19 | - (date, required) 20 | 21 | ### Did this require a temporary workaround? If yes, what was it? 22 | - (Y/N, required) 23 | - (short-answer, optional) 24 | 25 | ### Who was involved 26 | - (@name, optional) 27 | 28 | ### What happens if this is not addressed 29 | - (short-answer, required) 30 | 31 | ### Resources 32 | - (hyperlink-list, optional) 33 | 34 | ### Recommended Action Items 35 | - (checkbox-list, optional) 36 | - [ ] Share issue link with team Lead 37 | - [ ] Add to upcoming agenda 38 | 39 | ### Potential Solutions 40 | - (short-answer, optional) 41 | -------------------------------------------------------------------------------- /src/components/common/MultiSelect/SelectItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import MenuItem from '@mui/material/MenuItem'; 4 | import makeStyles from '@mui/styles/makeStyles'; 5 | 6 | const useStyles = makeStyles(() => ({ 7 | selectItemTextWrap: { 8 | whiteSpace: 'normal', 9 | }, 10 | })); 11 | 12 | function SelectItem({ 13 | text, value, onClick, disabled, 14 | }) { 15 | const classes = useStyles(); 16 | return ( 17 | 27 | {text} 28 | 29 | ); 30 | } 31 | 32 | export default SelectItem; 33 | 34 | SelectItem.propTypes = { 35 | text: PropTypes.string, 36 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, 37 | onClick: PropTypes.func.isRequired, 38 | disabled: PropTypes.bool, 39 | }; 40 | 41 | SelectItem.defaultProps = { 42 | text: '', 43 | disabled: false, 44 | }; 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/blank-dev-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Blank Dev Issue 3 | about: Template for developers to create issues and prepare for engineering tickets. 4 | title: '' 5 | labels: 'Complexity: Missing, draft, Feature: Missing, Milestone: Missing, Role: Frontend, 6 | size: Missing' 7 | assignees: '' 8 | 9 | --- 10 | 11 | ### Overview 12 | 13 | We need to do X for Y reason. 14 | 15 | ### More Info (optional) 16 | 17 | Explain here if the issue requires additional info 18 | 19 | ### Action Items 20 | 21 | - [ ] Break the issue down into manageable and actionable steps here 22 | - [ ] action 2 23 | - [ ] action 3 24 | - [ ] action 4 25 | 26 | ### Resources/Instructions 27 | 28 | - Figma Section: 29 | - Related Design Ticket: 30 | 31 |
    Screenshot before proposed changes 32 |

    33 | [insert screenshot here] 34 |

    35 |
    36 | 37 |
    Screenshot of Figma 38 |

    39 | [insert screenshot here] 40 |

    41 |
    42 | 43 |
    Screenshot of new feature from localhost 44 |

    45 | [insert screenshot here] 46 |

    47 |
    48 | -------------------------------------------------------------------------------- /src/features/Map/controls/RequestsBarChart.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import React from 'react'; 4 | import PropTypes from 'proptypes'; 5 | import { REQUEST_TYPES } from '@components/common/CONSTANTS'; 6 | 7 | const RequestsBarChart = ({ selectedRequests }) => { 8 | const max = Math.max(...Object.values(selectedRequests)); 9 | return ( 10 |
    11 | { Object.keys(REQUEST_TYPES).map(type => { 12 | if (Object.keys(selectedRequests).includes(type)) 13 | return ( 14 |
    15 |
    16 | { type } ({selectedRequests[type]}) 17 |
    18 |
    25 |
    26 | ) 27 | else 28 | return null; 29 | })} 30 |
    31 | ); 32 | } 33 | 34 | export default RequestsBarChart; 35 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import 'core-js/stable'; 2 | import 'regenerator-runtime/runtime'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { Provider } from 'react-redux'; 6 | import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles'; 7 | import { CssBaseline } from '@mui/material'; 8 | import DbProvider from '@db/DbProvider'; 9 | import theme from '@theme/theme'; 10 | import store from '@src/redux/store'; 11 | import App from '@src/App'; 12 | 13 | if (import.meta.env.DEV && !import.meta.env.VITE_MAPBOX_TOKEN) { 14 | alert('Missing Mapbox token. Please run `npm run setup`.') 15 | } 16 | 17 | // Expose theme to debugging console like on mui.com. 18 | // https://mui.com/material-ui/customization/typography/#default-values 19 | window.theme = theme; 20 | 21 | ReactDOM.render( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | , 32 | document.getElementById('root'), 33 | ); 34 | -------------------------------------------------------------------------------- /src/components/common/MultiSelect/SelectGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | 5 | import SelectItem from './SelectItem'; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | groupName: { 9 | ...theme.typography.body2, 10 | marginTop: theme.gaps.sm, 11 | marginBottom: theme.gaps.xs, 12 | }, 13 | })); 14 | 15 | function SelectGroup({ name, items, onChange }) { 16 | const classes = useStyles(); 17 | 18 | return ( 19 |
    20 |
    {`${name} (${items.length})`}
    21 | {items.map(item => ( 22 | 28 | ))} 29 |
    30 | ); 31 | } 32 | 33 | export default SelectGroup; 34 | 35 | SelectGroup.propTypes = { 36 | name: PropTypes.string.isRequired, 37 | items: PropTypes.arrayOf(PropTypes.shape({})), 38 | onChange: PropTypes.func, 39 | }; 40 | 41 | SelectGroup.defaultProps = { 42 | items: [], 43 | onChange: () => null, 44 | }; 45 | -------------------------------------------------------------------------------- /.github/workflows/build_test_backend.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: 3 | push: 4 | paths: 5 | - 'server/**' 6 | defaults: 7 | run: 8 | shell: bash 9 | working-directory: server 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Install and configure API 16 | run: | 17 | cp .env.example .env 18 | docker-compose up --no-start api 19 | - name: Run API (w/docker debugging) 20 | run: | 21 | docker-compose up -d api 22 | docker ps -a 23 | docker network inspect 311_data_default 24 | nc -zv localhost 5432 25 | docker-compose logs db 26 | docker-compose logs api 27 | - name: Lint API 28 | run: docker-compose run api flake8 29 | - name: Seed the test database 30 | run: docker-compose run -e TESTING=True api alembic upgrade head 31 | - name: Run unit tests 32 | run: | 33 | docker-compose run api pytest 34 | docker-compose run prefect pytest 35 | # - name: Run Postman tests 36 | # run: chmod +x postman/test.sh && postman/test.sh 37 | -------------------------------------------------------------------------------- /src/theme/typography.js: -------------------------------------------------------------------------------- 1 | import fonts from '@theme/fonts'; 2 | 3 | // Note to future maintainers... 4 | 5 | // Ideally, define only font-size in the typography object below. 6 | 7 | // Modifiers like font-weight and font-family, are component specific. 8 | // Therefore, consider defining them in the className property of the 9 | // component via makeStyles(). 10 | 11 | // Example: 12 | 13 | const typography = { 14 | button: { 15 | textTransform: 'none', 16 | }, 17 | 18 | // Default family and weight for everything below. 19 | fontFamily: fonts.family.roboto, 20 | fontWeight: fonts.weight.regular, 21 | 22 | h1: { 23 | fontSize: 96, 24 | }, 25 | h2: { 26 | fontSize: 60, 27 | }, 28 | h3: { 29 | fontSize: 46, 30 | }, 31 | h4: { 32 | fontSize: 36, 33 | }, 34 | h5: { 35 | fontSize: 24, 36 | }, 37 | h6: { 38 | fontSize: 18, 39 | }, 40 | body1: { 41 | fontSize: 16, 42 | }, 43 | body2: { 44 | fontSize: 14, 45 | }, 46 | subtitle1: { 47 | fontSize: 16, 48 | }, 49 | subtitle2: { 50 | fontSize: 21, 51 | }, 52 | }; 53 | 54 | export default typography; 55 | -------------------------------------------------------------------------------- /src/components/layout/ContentBody/index.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Container from '@mui/material/Container'; 4 | import Grid from '@mui/material/Grid'; 5 | import clsx from 'clsx'; 6 | import sharedLayout from '@theme/layout'; 7 | 8 | // ContentBody keeps the body of all content pages centered 9 | // with a customizable maxWidth container that defaults to 'md'. 10 | 11 | function ContentBody({ children, maxWidth, hasTopMargin }) { 12 | const classes = sharedLayout(); 13 | 14 | return ( 15 | 16 | 17 | 18 |
    19 | {children} 20 |
    21 |
    22 |
    23 |
    24 | ); 25 | } 26 | 27 | ContentBody.defaultProps = { 28 | children: {}, 29 | maxWidth: 'md', 30 | hasTopMargin: true, 31 | }; 32 | 33 | ContentBody.propTypes = { 34 | children: PropTypes.node, 35 | maxWidth: PropTypes.string, 36 | hasTopMargin: PropTypes.bool, 37 | }; 38 | 39 | export default ContentBody; 40 | -------------------------------------------------------------------------------- /src/scripts/csv_debug_tools/add_na_column.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This script is for adding 'N/A' values to the 8th column 'CreatedByUserOrganization' 3 | Due to 2021 data missing values in that entire column, which shifted all columns 4 | after it forward 5 | Saving this for future similar situation 6 | ''' 7 | 8 | import csv 9 | 10 | input_file = "2021.csv" 11 | output_file = "2021_with_na.csv" 12 | 13 | with open(input_file, "r", newline='', encoding='utf-8') as infile, open(output_file, "w", newline='', encoding='utf-8') as outfile: 14 | reader = csv.reader(infile) 15 | writer = csv.writer(outfile) 16 | 17 | # Read the header 18 | header = next(reader) 19 | writer.writerow(header) 20 | 21 | for line_number, row in enumerate(reader, start=2): 22 | # Ensure row has the correct length by adding 'N/A' to the 8th column if necessary 23 | if len(row) != len(header): 24 | if len(row) == len(header) - 1: 25 | row.insert(8, 'N/A') 26 | else: 27 | print(f"Line {line_number} has an incorrect number of columns: {len(row)} instead of {len(header)}") 28 | writer.writerow(row) 29 | 30 | print(f"Processed {input_file} and saved to {output_file}") 31 | -------------------------------------------------------------------------------- /src/assets/round-check-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/contact/ContactImage.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import coverImage from '@assets/contact_bg.png'; 5 | 6 | const useStyles = makeStyles(theme => ({ 7 | contactImageCover: { 8 | height: '25vh', 9 | backgroundImage: `url(${coverImage})`, 10 | backgroundPosition: 'top', 11 | backgroundRepeat: 'no-repeat', 12 | backgroundSize: 'cover', 13 | position: 'relative', 14 | }, 15 | contactImageOverlayText: { 16 | color: theme.palette.background.default, 17 | left: '50%', 18 | fontSize: '40px', 19 | fontWeight: 'bold', 20 | position: 'absolute', 21 | textAlign: 'center', 22 | top: '50%', 23 | transform: 'translate(-50%, -50%)', 24 | }, 25 | })); 26 | 27 | function ContactImage({ children }) { 28 | const classes = useStyles(); 29 | 30 | return ( 31 |
    32 |
    33 | {children} 34 |
    35 |
    36 | ); 37 | } 38 | 39 | ContactImage.propTypes = { 40 | children: PropTypes.node.isRequired, 41 | }; 42 | 43 | export default ContactImage; 44 | -------------------------------------------------------------------------------- /src/features/Map/districts.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import { COUNCILS, CITY_COUNCILS } from '@components/common/CONSTANTS'; 4 | import ncGeoJson from '@data/nc-boundary-2019-modified.json'; 5 | import ccGeoJson from '@data/la-city-council-districts-2012.json'; 6 | import { isPointWithinGeo } from './geoUtils'; 7 | 8 | export const ncBoundaries = ncGeoJson; 9 | export const ccBoundaries = ccGeoJson; 10 | 11 | export function ncNameFromId(ncId) { 12 | return COUNCILS.find(c => c.id == ncId)?.name; 13 | } 14 | 15 | export function ccNameFromId(ccId) { 16 | return CITY_COUNCILS.find(c => c.id == ccId)?.name; 17 | } 18 | 19 | export function ncInfoFromLngLat({ lng, lat }) { 20 | for (let i = 0; i < ncBoundaries.features.length; i++) { 21 | const feature = ncBoundaries.features[i]; 22 | if (isPointWithinGeo([lng, lat], feature)) 23 | return { 24 | name: ncNameFromId(feature.properties.nc_id), 25 | url: feature.properties.waddress || feature.properties.dwebsite, 26 | }; 27 | } 28 | return null; 29 | } 30 | 31 | export function ccNameFromLngLat({ lng, lat }) { 32 | for (let i = 0; i < ccBoundaries.features.length; i++) 33 | if (isPointWithinGeo([lng, lat], ccBoundaries.features[i])) 34 | return ccNameFromId(ccBoundaries.features[i].properties.name); 35 | return null; 36 | } 37 | -------------------------------------------------------------------------------- /src/redux/reducers/mapFilters.js: -------------------------------------------------------------------------------- 1 | import { REQUEST_TYPES, MAP_DATE_RANGES, COUNCILS } from '@components/common/CONSTANTS'; 2 | 3 | export const types = { 4 | UPDATE_MAP_DATE_RANGE: 'UPDATE_MAP_DATE_RANGE', 5 | }; 6 | 7 | export const updateMapDateRange = ({ dateRange, startDate, endDate }) => ({ 8 | type: types.UPDATE_MAP_DATE_RANGE, 9 | payload: { dateRange, startDate, endDate }, 10 | }); 11 | 12 | // set all types to either true or false 13 | const allRequestTypes = value => ( 14 | Object.keys(REQUEST_TYPES).reduce((acc, type) => { 15 | acc[type] = value; 16 | return acc; 17 | }, { All: value }) 18 | ); 19 | 20 | const allCouncils = COUNCILS.map(council => council.name); 21 | 22 | const initialState = { 23 | dateRange: MAP_DATE_RANGES[0].id, 24 | startDate: MAP_DATE_RANGES[0].startDate, 25 | endDate: MAP_DATE_RANGES[0].endDate, 26 | councils: allCouncils, 27 | requestTypes: allRequestTypes(true), 28 | }; 29 | 30 | export default (state = initialState, action) => { 31 | switch (action.type) { 32 | case types.UPDATE_MAP_DATE_RANGE: { 33 | const { dateRange, startDate, endDate } = action.payload; 34 | return { 35 | ...state, 36 | dateRange, 37 | startDate, 38 | endDate, 39 | }; 40 | } 41 | default: 42 | return state; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/assets/typcn_export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/features/Map/zoomTooltip.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Tooltip from '@mui/material/Tooltip'; 4 | 5 | function ZoomTooltip({ show }) { 6 | return ( 7 | 13 |

    14 | 15 | Zoom features are limited while locked into a 16 | neighborhood council. 17 | 18 |
    19 | To reset zoom features, please exit the boundary selection 20 | by clicking the 'X' on the selected Neighborhood Council 21 | within the 'Boundaries' filter of the 'Search & Filters' 22 | modal. 23 |

    24 |
    25 | )} 26 | //* changing styles here changes the color of the zoom control, not the tooltip 27 | > 28 | {/* empty span for positioning the zoomtooltip */} 29 | 34 | 35 | ); 36 | } 37 | ZoomTooltip.propTypes = { 38 | show: PropTypes.bool.isRequired, 39 | }; 40 | 41 | export default ZoomTooltip; 42 | -------------------------------------------------------------------------------- /src/components/layout/Main/Desktop/CouncilSelector/SelectedCouncils.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import ChipList, { StyledChip } from '@components/common/ChipList'; 5 | 6 | const useStyles = makeStyles(theme => ({ 7 | placeholder: { 8 | ...theme.typography.body2, 9 | fontSize: '12px', 10 | color: theme.palette.text.secondaryDark, 11 | }, 12 | })); 13 | 14 | function SelectedCouncils({ items, onDelete }) { 15 | const classes = useStyles(); 16 | 17 | const renderSelected = () => items.map(item => ( 18 | 25 | )); 26 | 27 | return ( 28 | 29 | {items.length ? ( 30 | renderSelected() 31 | ) : ( 32 | Neighborhood Councils 33 | )} 34 | 35 | ); 36 | } 37 | 38 | export default SelectedCouncils; 39 | 40 | SelectedCouncils.propTypes = { 41 | onDelete: PropTypes.func.isRequired, 42 | items: PropTypes.arrayOf( 43 | PropTypes.shape({ 44 | councilId: PropTypes.number, 45 | councilName: PropTypes.string, 46 | color: PropTypes.string, 47 | }), 48 | ).isRequired, 49 | }; 50 | -------------------------------------------------------------------------------- /src/redux/sagas/metadata.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, put, all } from 'redux-saga/effects'; 2 | import { removeFromName, truncateName } from '@utils'; 3 | import settings from '@settings'; 4 | 5 | import councils from '@db/councils'; 6 | import regions from '@db/regions'; 7 | import agencies from '@db/agencies'; 8 | import metadata from '@db/metadata'; 9 | import ncGeojson from '@db/ncGeojson'; 10 | 11 | import { 12 | types, 13 | getMetadataSuccess, 14 | getCouncilsSuccess, 15 | getRegionsSuccess, 16 | getNcGeojsonSuccess, 17 | 18 | 19 | getAgenciesSuccess, 20 | getMetadataFailure, 21 | } from '../reducers/metadata'; 22 | 23 | const getFormattedCouncilNames = () => councils.map(councilObj => ({ 24 | ...councilObj, 25 | councilName: truncateName( 26 | removeFromName(councilObj.councilName, ['OWERMENT', 'GRESS', ' NC']), 27 | settings.selectItem.maxLen, 28 | ), 29 | })); 30 | 31 | function* getMetadata() { 32 | try { 33 | yield all([ 34 | put(getMetadataSuccess(metadata)), 35 | put(getCouncilsSuccess(getFormattedCouncilNames())), 36 | put(getRegionsSuccess(regions)), 37 | put(getAgenciesSuccess(agencies)), 38 | put(getNcGeojsonSuccess(ncGeojson)), 39 | ]); 40 | } catch (e) { 41 | yield put(getMetadataFailure(e)); 42 | } 43 | } 44 | 45 | export default function* rootSaga() { 46 | yield takeLatest(types.GET_METADATA_REQUEST, getMetadata); 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/Continuous_Integration_Frontend.yml: -------------------------------------------------------------------------------- 1 | name: CI_Frontend 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'client/**' 7 | - '.github/workflows/Continuous_Integration_Frontend.yml' 8 | workflow_dispatch: 9 | 10 | defaults: 11 | run: 12 | working-directory: client 13 | 14 | jobs: 15 | ci_frontend_build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v1 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 12 22 | - name: Install Packages 23 | run: npm install 24 | - name: Lint 25 | run: npm run lint 26 | - name: Setup environment 27 | run: | 28 | echo VITE_MAPBOX_TOKEN=${{ secrets.VITE_MAPBOX_TOKEN }} > .env 29 | echo MAPBOX_STREETS_URL=${{ secrets.MAPBOX_STREETS_URL }} >> .env 30 | echo MAPBOX_SATELLITE_URL=${{ secrets.MAPBOX_SATELLITE_URL }} >> .env 31 | echo API_URL=${{ secrets.API_URL_PROD }} >> .env 32 | echo REPORT_URL=${{ secrets.REPORT_URL }} >> .env 33 | echo SENTRY_CLIENT_DSN=${{ secrets.SENTRY_CLIENT_DSN }} >> .env 34 | echo MIXPANEL_ENABLED=${{ secrets.MIXPANEL_ENABLED }} >> .env 35 | echo MIXPANEL_TOKEN_PROD=${{ secrets.MIXPANEL_TOKEN_PROD }} >> .env 36 | echo MIXPANEL_TOKEN_DEV=${{ secrets.MIXPANEL_TOKEN_DEV }} >> .env 37 | - name: Build project 38 | run: npm run build 39 | - name: Run Tests 40 | run: npm run test -- --coverage 41 | -------------------------------------------------------------------------------- /src/pages/Privacy/Privacy.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import Grid from '@mui/material/Grid'; 4 | import makeStyles from '@mui/styles/makeStyles'; 5 | import sharedLayout from '@theme/layout'; 6 | import TextHeading from '@components/layout/TextHeading'; 7 | import ContentBody from '@components/layout/ContentBody'; 8 | import useContentful from '../../hooks/useContentful'; 9 | 10 | const useStyles = makeStyles({ 11 | contentBody: { 12 | fontSize: '1rem', 13 | }, 14 | }); 15 | 16 | const query = ` 17 | query { 18 | privacyPolicy(id: "2HBnjUBMJ7KNimZGjhAsEi") { 19 | body 20 | } 21 | } 22 | `; 23 | 24 | function Privacy() { 25 | const { data, errors } = useContentful(query); 26 | const classes = { ...useStyles(), ...sharedLayout() }; 27 | 28 | React.useEffect(() => { 29 | if (errors) console.log(errors); 30 | }, [errors]); 31 | 32 | return ( 33 | <> 34 | 35 | Privacy Policy 36 | 37 | 38 | 39 | { data 40 | && ( 41 | 42 | 43 | 44 | {data.privacyPolicy.body} 45 | 46 | 47 | 48 | )} 49 | 50 | 51 | ); 52 | } 53 | 54 | export default Privacy; 55 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | 5 | export default defineConfig(() => { 6 | return { 7 | base: '/311-data/', 8 | build: { 9 | outDir: 'dist', 10 | }, 11 | plugins: [react()], 12 | resolve: { 13 | alias: { 14 | '@root': __dirname, 15 | '@src': path.resolve(__dirname, "src/"), 16 | '@data': path.resolve(__dirname, 'backend/'), 17 | '@theme': path.resolve(__dirname, 'src/theme'), 18 | '@components': path.resolve(__dirname, 'src/components'), 19 | '@dashboards': path.resolve(__dirname, 'src/components/Dashboards'), 20 | '@hooks': path.resolve(__dirname, 'src/hooks'), 21 | '@reducers': path.resolve(__dirname, 'src/redux/reducers'), 22 | '@styles': path.resolve(__dirname, 'styles'), 23 | '@assets': path.resolve(__dirname, 'src/assets'), 24 | '@utils': path.resolve(__dirname, 'src/utils'), 25 | '@settings': path.resolve(__dirname, 'src/settings'), 26 | '@db': path.resolve(__dirname, 'backend/'), 27 | '@routes': path.resolve(__dirname, 'src/routes/'), 28 | '@features': path.resolve(__dirname, 'src/features'), 29 | '@pages': path.resolve(__dirname, 'src/pages'), 30 | }, 31 | }, 32 | test: { 33 | environment: 'jsdom', 34 | setupFiles: 'utils/test-setup.js' 35 | } 36 | }; 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/contact/ContactIntro.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Grid from '@mui/material/Grid'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import Typography from '@mui/material/Typography'; 5 | import sharedLayout from '@theme/layout'; 6 | import { Box } from '@mui/material'; 7 | import typography from '@theme/typography'; 8 | 9 | const useStyles = makeStyles(theme => ({ 10 | contentTitle: { 11 | fontSize: typography.h6.fontSize, 12 | fontWeight: theme.typography.fontWeightMedium, 13 | paddingTop: theme.spacing(10) 14 | }, 15 | contentSentence: { 16 | paddingBottom: theme.spacing(10), 17 | textAlign: "center" 18 | } 19 | })); 20 | 21 | function ContactIntro() { 22 | const classes = { ...useStyles(), ...sharedLayout() }; 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | {'Don\'t See What You Need?'} 30 | 31 | 32 |
    33 | 34 | We are open to suggestions and feedback and would love the opportunity to get connected. 35 | 36 |
    37 |
    38 |
    39 | ); 40 | } 41 | 42 | export default ContactIntro; 43 | -------------------------------------------------------------------------------- /src/components/common/ArrowToolTip/index.jsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles'; 2 | import PropTypes from 'prop-types'; 3 | import Tooltip, { tooltipClasses } from '@mui/material/Tooltip'; 4 | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; 5 | import React from 'react'; 6 | 7 | const ArrowToolTip = ({iconStyle, children}) => { 8 | 9 | const StyledToolTip = styled(({ className }) => ( 10 | 16 | 20 | 21 | ))(({ theme }) => ({ 22 | [`& .${tooltipClasses.arrow}`]: { 23 | '&::before': { 24 | backgroundColor: theme.palette.common.white, 25 | }, 26 | }, 27 | [`& .${tooltipClasses.tooltip}`]: { 28 | backgroundColor: theme.palette.common.white, 29 | color: theme.palette.common.black, 30 | marginLeft: '-4px', 31 | maxWidth: '275px', 32 | padding: '5px', 33 | boxShadow: ` 34 | 0 4px 4px 0 rgba(0, 0, 0, 0.25), 35 | 0 8px 10px 0 rgba(0, 0, 0, 0.14), 36 | 0 3px 14px 0 rgba(0, 0, 0, 0.12) 37 | `, 38 | }, 39 | })); 40 | 41 | return ; 42 | }; 43 | 44 | export default ArrowToolTip; 45 | ArrowToolTip.propTypes = { 46 | children: PropTypes.node.isRequired, 47 | iconStyles: PropTypes.string, 48 | } -------------------------------------------------------------------------------- /src/pages/Home/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import makeStyles from '@mui/styles/makeStyles'; 3 | 4 | import PropTypes from 'prop-types'; 5 | import MapContainer from '@features/Map'; 6 | import PersistentDrawerLeft from '../../components/layout/Main/shared/PersistentDrawerLeft'; 7 | 8 | const useStyles = makeStyles(theme => ({ 9 | root: { 10 | position: 'absolute', 11 | top: theme.header.height, 12 | bottom: theme.footer.height, 13 | left: 0, 14 | right: 0, 15 | }, 16 | })); 17 | 18 | function Desktop({ initialState }) { 19 | const classes = useStyles(); 20 | return ( 21 |
    22 | 23 | 24 |
    25 | ); 26 | } 27 | 28 | export default Desktop; 29 | 30 | Desktop.propTypes = { 31 | initialState: PropTypes.shape({ 32 | councilId: PropTypes.string, 33 | rtId1: PropTypes.string, 34 | rtId2: PropTypes.string, 35 | rtId3: PropTypes.string, 36 | rtId4: PropTypes.string, 37 | rtId5: PropTypes.string, 38 | rtId6: PropTypes.string, 39 | rtId7: PropTypes.string, 40 | rtId8: PropTypes.string, 41 | rtId9: PropTypes.string, 42 | rtId10: PropTypes.string, 43 | rtId11: PropTypes.string, 44 | rtId12: PropTypes.string, 45 | requestStatusOpen: PropTypes.string, 46 | requestStatusClosed: PropTypes.string, 47 | startDate: PropTypes.string, 48 | endDate: PropTypes.string, 49 | }).isRequired, 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/DateSelector/useStyles.js: -------------------------------------------------------------------------------- 1 | import makeStyles from '@mui/styles/makeStyles'; 2 | 3 | const useStyles = makeStyles(theme => ({ 4 | label: { 5 | marginBottom: 5, 6 | display: 'inline-block', 7 | fontFamily: 'Roboto', 8 | }, 9 | selector: { 10 | fontFamily: 'Roboto', 11 | display: 'flex', 12 | justifyContent: 'space-between', 13 | alignItems: 'center', 14 | '& > div': { 15 | paddingTop: 0, 16 | paddingRight: 0, 17 | paddingBottom: 0, 18 | }, 19 | marginLeft: -10, 20 | }, 21 | separator: { 22 | marginLeft: theme.gaps.md, 23 | borderRight: `1.5px solid ${theme.palette.text.secondaryLight}`, 24 | height: '1.2rem', 25 | }, 26 | option: { 27 | cursor: 'pointer', 28 | padding: 6, 29 | margin: '2px 0', 30 | fontFamily: 'Roboto', 31 | width: '100%', 32 | backgroundColor: theme.palette.primary.dark, 33 | border: 'none', 34 | textAlign: 'left', 35 | color: theme.palette.text.secondaryLight, 36 | '&:hover': { 37 | backgroundColor: theme.palette.selected.primary, 38 | }, 39 | }, 40 | selected: { 41 | backgroundColor: `${theme.palette.selected.primary} !important`, 42 | }, 43 | tooltipParagraph: { 44 | margin: '1px', 45 | }, 46 | iconStyle: { 47 | verticalAlign: 'middle', 48 | }, 49 | header: { 50 | fontSize: '12.47px', 51 | fontWeight: theme.typography.fontWeightMedium, 52 | marginBottom: '8px', 53 | }, 54 | })); 55 | 56 | export default useStyles; 57 | -------------------------------------------------------------------------------- /src/components/Loading/FunFactCard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { styled } from '@mui/material/styles'; 3 | import { Card, Box, Typography } from '@mui/material'; 4 | import { seconds } from '@utils'; 5 | import facts from '@data/facts'; 6 | 7 | const StyledCard = styled(Card)({ 8 | display: 'flex', 9 | alignItems: 'flex', 10 | justifyContent: 'center', 11 | }); 12 | 13 | const StyledBox = styled(Box)(({ theme }) => ({ 14 | position: 'absolute', 15 | bottom: '12vh', 16 | backgroundColor: '#424242', 17 | padding: theme.spacing(3, 3, 3), 18 | boxShadow: theme.shadows[5], 19 | textAlign: 'center', 20 | maxWidth: '533px', 21 | width: 'auto', 22 | borderRadius: '10px', 23 | zIndex: 50000, // This prevents from being overlay by LoadingModal's backdrop 24 | })); 25 | 26 | export default function FunFactCard() { 27 | const [currentFactIndex, setCurrentFactIndex] = useState(0); 28 | const factsLength = facts.length; 29 | 30 | useEffect(() => { 31 | const intervalId = setInterval(() => { 32 | setCurrentFactIndex(prevIndex => (prevIndex + 1) % factsLength); 33 | }, seconds(5)); 34 | return () => clearInterval(intervalId); 35 | }, [factsLength]); 36 | 37 | return ( 38 | 39 | 40 | 41 | Did you know? 42 | {' '} 43 | {facts[currentFactIndex]} 44 | 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/layout/Main/Desktop/Export/useStyles.js: -------------------------------------------------------------------------------- 1 | import makeStyles from '@mui/styles/makeStyles'; 2 | 3 | const useStyles = makeStyles(theme => ({ 4 | exportButton: { 5 | color: theme.palette.text.secondaryLight, 6 | textDecoration: 'underline', 7 | '&:hover': { 8 | textDecoration: 'underline', 9 | }, 10 | padding: 0, 11 | }, 12 | confirmationButton: { 13 | width: '229px', 14 | height: '30px', 15 | borderRadius: '5px', 16 | border: '1px solid #ECECEC', 17 | '&:hover': { 18 | backgroundColor: '#DADADA', 19 | borderColor: '#DADADA', 20 | }, 21 | fontWeight: '500', 22 | fontSize: '18px', 23 | marginTop: '10px', 24 | }, 25 | confirmationOk: { 26 | backgroundColor: theme.palette.secondary.light, 27 | color: '#29404F', 28 | }, 29 | confirmationCancel: { 30 | backgroundColor: '#29404F', 31 | color: theme.palette.text.secondaryLight, 32 | }, 33 | warningButton: { 34 | width: '169px', 35 | height: '25px', 36 | borderRadius: '5px', 37 | backgroundColor: theme.palette.secondary.light, 38 | border: '1px solid #ECECEC', 39 | '&:hover': { 40 | backgroundColor: '#DADADA', 41 | borderColor: '#DADADA', 42 | }, 43 | color: '#29404F', 44 | fontWeight: '500', 45 | fontSize: '18px', 46 | marginTop: '10px', 47 | paddingTop: '8px', 48 | }, 49 | imageIcon: { 50 | display: 'flex', 51 | height: 'inherit', 52 | width: 'inherit', 53 | }, 54 | })); 55 | 56 | export default useStyles; 57 | -------------------------------------------------------------------------------- /src/components/layout/Footer/SocialMediaLinks.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import makeStyles from '@mui/styles/makeStyles'; 3 | 4 | import TwitterSVG from '@assets/twitter-round.svg'; 5 | import FacebookSVG from '@assets/facebook-round.svg'; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | socialMedia: { 9 | display: 'flex', 10 | flexDirection: 'row', 11 | lineHeight: theme.footer.height, 12 | justifyContent: 'space-between', 13 | width: '37px', 14 | marginLeft: '5px', 15 | alignContent: 'center', 16 | }, 17 | image: { 18 | filter: 19 | ' invert(85%) sepia(0%) saturate(0%) hue-rotate(53deg) brightness(94%) contrast(88%)', 20 | width: '16px', 21 | height: '16px', 22 | verticalAlign: 'middle', 23 | }, 24 | })); 25 | 26 | function SocialMediaLinks() { 27 | const classes = useStyles(); 28 | return ( 29 | 47 | ); 48 | } 49 | 50 | export default SocialMediaLinks; 51 | -------------------------------------------------------------------------------- /src/scripts/csv_debug_tools/inspect_csv.py: -------------------------------------------------------------------------------- 1 | """ 2 | Find and print problematic lines in a CSV file that do not match the expected number of columns. 3 | 4 | Parameters: 5 | - file_path: The path to the CSV file. 6 | - expected_columns: The expected number of columns in each row. 7 | - num_lines: The number of problematic lines to print. 8 | 9 | Example commmand: `python3 inspect_csv.py 2021.csv 34 5` 10 | """ 11 | 12 | import sys 13 | 14 | def find_problematic_line(file_path, expected_columns=34, num_lines=5): 15 | problematic_lines = [] 16 | 17 | with open(file_path, "r") as file: 18 | for line_number, line in enumerate(file, start=1): 19 | columns = line.strip().split(',') 20 | if len(columns) != expected_columns: 21 | problematic_lines.append((line_number, line.strip())) 22 | if len(problematic_lines) >= num_lines: 23 | break 24 | 25 | if problematic_lines: 26 | print(f"First {num_lines} problematic lines found:") 27 | for line_number, line in problematic_lines: 28 | print(f"Problematic line {line_number}: {line}") 29 | else: 30 | print("No problematic lines found.") 31 | 32 | if __name__ == "__main__": 33 | if len(sys.argv) != 4: 34 | print("Usage: python check_problematic_lines.py ") 35 | sys.exit(1) 36 | 37 | file_path = sys.argv[1] 38 | expected_columns = int(sys.argv[2]) 39 | num_lines = int(sys.argv[3]) 40 | 41 | find_problematic_line(file_path, expected_columns, num_lines) 42 | -------------------------------------------------------------------------------- /src/assets/address-icon-48.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/utils/duckDbHelpers.js: -------------------------------------------------------------------------------- 1 | import { createObjFromArrays } from '@utils'; 2 | 3 | // MetatableData 4 | const getTableSchema = table => { 5 | if (!!table?.schema?.fields === false) { 6 | return undefined; 7 | } 8 | const pairs = table.schema.fields.map(f => [f.name, f.type.toString()]); 9 | return Object.fromEntries(pairs); 10 | }; 11 | 12 | const getTableHeaders = table => { 13 | if (!!table?.schema?.fields === false) { 14 | return undefined; 15 | } 16 | const tableHeaders = table.schema.fields.map(f => f.name); 17 | return tableHeaders; 18 | }; 19 | 20 | // Records Count 21 | const getTableCount = table => { 22 | if (!!table === false) { 23 | return undefined; 24 | } 25 | return table.toArray().length; 26 | }; 27 | 28 | const getTableData = table => { 29 | if (!!table === false) { 30 | return undefined; 31 | } 32 | 33 | let currentRow = []; 34 | let currentRowAsObj = {}; 35 | const tableData = []; 36 | const tableHeaders = getTableHeaders(table); 37 | const nRows = getTableCount(table); 38 | 39 | // Loop through each row and create a javascript object 40 | // using tableHeaders for the key values for each corresponding value in 41 | // the current table row 42 | for (let i = 0; i < nRows; i += 1) { 43 | currentRow = table.get(i).toArray(); 44 | currentRowAsObj = createObjFromArrays({ 45 | keyArray: tableHeaders, 46 | valArray: currentRow, 47 | }); 48 | tableData.push(currentRowAsObj); 49 | } 50 | 51 | return tableData; 52 | }; 53 | 54 | export default { 55 | getTableCount, 56 | getTableData, 57 | getTableHeaders, 58 | getTableSchema, 59 | }; 60 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: ['airbnb', 'eslint:recommended', 'plugin:react/recommended'], 8 | globals: { 9 | Atomics: 'readonly', 10 | SharedArrayBuffer: 'readonly', 11 | }, 12 | parser: '@babel/eslint-parser', 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | ecmaVersion: 2018, 18 | sourceType: 'module', 19 | }, 20 | settings: { 21 | 'import/extensions': ['.js', '.jsx'], 22 | 'import/resolver': { 23 | node: {}, 24 | }, 25 | }, 26 | plugins: ['react', 'react-hooks'], 27 | rules: { 28 | 'linebreak-style': 'off', 29 | 'react-hooks/rules-of-hooks': 'error', 30 | 'react-hooks/exhaustive-deps': 'warn', 31 | 'arrow-parens': ['error', 'as-needed'], 32 | indent: [ 33 | 'error', 34 | 2, 35 | { 36 | SwitchCase: 1, 37 | MemberExpression: 'off', 38 | ignoredNodes: ['TemplateLiteral'], 39 | }, 40 | ], 41 | 'template-curly-spacing': 'off', 42 | 'jsx-a11y/no-noninteractive-tabindex': [ 43 | 'error', 44 | { 45 | tags: [], 46 | roles: ['tabpanel'], 47 | }, 48 | ], 49 | 'jsx-a11y/label-has-associated-control': [2, { 50 | labelComponents: ['label'], 51 | labelAttributes: ['htmlFor'], 52 | controlComponents: ['input'], 53 | }], 54 | 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }], 55 | 56 | 'default-param-last': 'warn', 57 | 'no-restricted-exports': 'warn', 58 | 'react/jsx-no-constructed-context-values': 'warn', 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/common/GearButton/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'proptypes'; 3 | import { IconButton } from '@mui/material'; 4 | import makeStyles from '@mui/styles/makeStyles'; 5 | import SettingsSharpIcon from '@mui/icons-material/SettingsSharp'; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | gearIcon: { 9 | color: theme.palette.text.dark, 10 | background: '#29404F', 11 | borderRadius: '12px', 12 | height: '33px', 13 | width: '33px', 14 | padding: '6px', 15 | }, 16 | button: { 17 | padding: '0', 18 | }, 19 | })); 20 | 21 | function GearButton({ 22 | onClick, 23 | }) { 24 | const { gearIcon, button } = useStyles(); 25 | const [pressed, setPressed] = useState(false); 26 | 27 | const onKeyDown = e => { 28 | e.preventDefault(); 29 | if (e.key === ' ' 30 | || e.key === 'Enter' 31 | || e.key === 'Spacebar' 32 | ) { 33 | setPressed(!pressed); 34 | onClick(); 35 | } 36 | }; 37 | 38 | const toggleClick = () => { 39 | setPressed(!pressed); 40 | onClick(); 41 | }; 42 | 43 | return ( 44 | 55 | 56 | 57 | ); 58 | } 59 | 60 | GearButton.propTypes = { 61 | onClick: PropTypes.func, 62 | }; 63 | GearButton.defaultProps = { 64 | onClick: undefined, 65 | }; 66 | 67 | export default GearButton; 68 | -------------------------------------------------------------------------------- /src/templates/contact-us.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 |
    6 |
    7 |

    {{to_name}},

    8 |

    9 | We appreciate you taking the time to leave a suggestion for our 311 Data service request map. 10 | Your feedback is important to us, and we are always looking for ways to improve our data visualization tool. 11 | Please follow our LinkedIn for updates on this civic tech project and others within the Hack for LA organization. 12 |

    13 | 14 |

    15 | Sincerely, 16 |
    17 | 311 Data Volunteers at Hack for LA. 18 |

    19 |
    20 |
    21 |
    22 | 23 | 24 | 25 |
    26 |
    27 |
    28 | 29 |
    Powered by Volunteers at Hack for LA
    30 |
    31 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/DateSelector/DateRanges.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | function DateRanges({ 5 | options, onSelect, dates, classes, 6 | }) { 7 | function highlightIfSelected(optionDays, selectedDays) { 8 | if (dates.length > 0) { 9 | const [from, to, start, end] = [ 10 | ...optionDays, 11 | ...selectedDays, 12 | ].map(date => date.toLocaleDateString('en-US')); 13 | const isSelected = from === start && to === end; 14 | 15 | if (isSelected) return classes.selected; 16 | } 17 | return ' '; 18 | } 19 | 20 | return ( 21 |
    22 | {options 23 | ? options.map(option => ( 24 | 37 | )) 38 | : null} 39 |
    40 | ); 41 | } 42 | 43 | const { 44 | func, arrayOf, shape, string, 45 | } = PropTypes; 46 | 47 | const Option = shape({ 48 | text: string, 49 | dates: arrayOf(Date), 50 | }); 51 | 52 | DateRanges.propTypes = { 53 | onSelect: func.isRequired, 54 | options: arrayOf(Option).isRequired, 55 | dates: arrayOf(Date), 56 | classes: PropTypes.shape({ 57 | selected: PropTypes.string, 58 | option: PropTypes.string, 59 | }), 60 | }; 61 | 62 | DateRanges.defaultProps = { 63 | classes: { selected: '', option: '' }, 64 | dates: [], 65 | }; 66 | 67 | export default DateRanges; 68 | -------------------------------------------------------------------------------- /src/components/common/SearchBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import SearchIcon from '@mui/icons-material/Search'; 5 | 6 | const useStyles = makeStyles(theme => ({ 7 | wrapper: { 8 | display: 'flex', 9 | borderBottom: `1px solid ${theme.palette.primary.focus}`, 10 | paddingBottom: theme.gaps.xs, 11 | }, 12 | input: { 13 | border: 'none', 14 | backgroundColor: theme.palette.primary.dark, 15 | color: theme.palette.text.secondaryLight, 16 | '&::placeholder': { 17 | ...theme.typography.body2, 18 | color: theme.palette.text.secondaryDark, 19 | }, 20 | paddingLeft: theme.gaps.xs, 21 | width: '100%', 22 | }, 23 | icon: { 24 | display: 'inline-block', 25 | fontSize: 20, 26 | color: theme.palette.primary.focus, 27 | }, 28 | })); 29 | 30 | function SearchBar({ 31 | value, 32 | placeholder, 33 | onChange, 34 | }) { 35 | const classes = useStyles(); 36 | 37 | return ( 38 |
    39 | 40 | onChange(e.target.value)} 46 | value={value} 47 | aria-label={placeholder} 48 | /> 49 |
    50 | ); 51 | } 52 | 53 | export default SearchBar; 54 | 55 | SearchBar.propTypes = { 56 | value: PropTypes.string, 57 | placeholder: PropTypes.string, 58 | onChange: PropTypes.func, 59 | }; 60 | 61 | SearchBar.defaultProps = { 62 | value: null, 63 | placeholder: 'Search', 64 | onChange: () => null, 65 | }; 66 | -------------------------------------------------------------------------------- /src/assets/twitter-round.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/layout/TextHeading/TextHeadingFAQ.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import Typography from '@mui/material/Typography'; 5 | import cityBackground from '@assets/city_background_header.png' 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | headingBackground: { 9 | background: `url(${cityBackground}) center 57% / cover`, 10 | backgroundPosition: 'center 57%', 11 | width: '100%', 12 | height: '240px', 13 | position: 'relative', 14 | }, 15 | backdrop: { 16 | width: '100%', 17 | height: '100%', 18 | background: `linear-gradient(180deg, rgba(53, 82, 129, 0.4) 50%, rgba(41, 64, 79, 0.8) 100%, rgba(29, 63, 90, 0.4) 100%)`, 19 | backgroundPosition: 'center', 20 | }, 21 | headingOverlayText: { 22 | left: '50%', 23 | color: 'white', 24 | position: 'absolute', 25 | textAlign: 'center', 26 | top: '50%', 27 | transform: 'translate(-50%, -70%)', 28 | }, 29 | contentHeading: { 30 | fontWeight: theme.typography.fontWeightBold, 31 | }, 32 | })); 33 | 34 | // TextHeading provides a standardized heading area and custom title 35 | // below the Header on all content pages. 36 | 37 | function TextHeadingFAQ({ children }) { 38 | const classes = useStyles(); 39 | 40 | return ( 41 |
    42 |
    43 |
    44 | 45 |
    {children}
    46 |
    47 |
    48 |
    49 | ); 50 | } 51 | 52 | TextHeadingFAQ.defaultProps = { 53 | children: {}, 54 | }; 55 | 56 | TextHeadingFAQ.propTypes = { 57 | children: PropTypes.node, 58 | }; 59 | 60 | export default TextHeadingFAQ; 61 | -------------------------------------------------------------------------------- /src/components/layout/TextHeading/index.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import Typography from '@mui/material/Typography'; 5 | import cityBackground from '@assets/city_background_header.png' 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | headingBackground: { 9 | background: `url(${cityBackground}) center 57% / cover`, 10 | backgroundPosition: 'center 57%', 11 | width: '100%', 12 | height: '240px', 13 | position: 'relative', 14 | }, 15 | backdrop: { 16 | width: '100%', 17 | height: '100%', 18 | background: `linear-gradient(180deg, rgba(53, 82, 129, 0.4) 50%, rgba(41, 64, 79, 0.8) 100%, rgba(29, 63, 90, 0.4) 100%)`, 19 | backgroundPosition: 'center', 20 | }, 21 | headingOverlayText: { 22 | left: '50%', 23 | color: 'white', 24 | position: 'absolute', 25 | textAlign: 'center', 26 | top: '50%', 27 | transform: 'translate(-50%, -70%)', 28 | zIndex: '1', 29 | }, 30 | contentHeading: { 31 | fontWeight: theme.typography.fontWeightBold, 32 | }, 33 | })); 34 | 35 | // TextHeading provides a standardized heading area and custom title 36 | // below the Header on all content pages. 37 | 38 | function TextHeading({ children }) { 39 | const classes = useStyles(); 40 | 41 | return ( 42 |
    43 |
    44 |
    45 | 46 |
    47 | {children} 48 |
    49 |
    50 |
    51 |
    52 | ); 53 | } 54 | 55 | TextHeading.defaultProps = { 56 | children: {}, 57 | }; 58 | 59 | TextHeading.propTypes = { 60 | children: PropTypes.node, 61 | }; 62 | 63 | export default TextHeading; 64 | -------------------------------------------------------------------------------- /backend/regions.js: -------------------------------------------------------------------------------- 1 | const regions = [ 2 | { 3 | regionId: 1, 4 | regionName: 'North East Valley', 5 | latitude: 34.261460423152776, 6 | longitude: -118.38246002952808, 7 | }, 8 | { 9 | regionId: 2, 10 | regionName: 'North West Valley', 11 | latitude: 34.26605708058946, 12 | longitude: -118.53919119943411, 13 | }, 14 | { 15 | regionId: 3, 16 | regionName: 'South West Valley', 17 | latitude: 34.18202774052978, 18 | longitude: -118.56342162172783, 19 | }, 20 | { 21 | regionId: 4, 22 | regionName: 'South East Valley', 23 | latitude: 34.17090721875934, 24 | longitude: -118.41457419030115, 25 | }, 26 | { 27 | regionId: 5, 28 | regionName: 'Central 1', 29 | latitude: 34.09594947025774, 30 | longitude: -118.33731631698825, 31 | }, 32 | { 33 | regionId: 6, 34 | regionName: 'Central 2', 35 | latitude: 34.04987724541125, 36 | longitude: -118.26900925500073, 37 | }, 38 | { 39 | regionId: 7, 40 | regionName: 'East', 41 | latitude: 34.108763950739, 42 | longitude: -118.27630256279464, 43 | }, 44 | { 45 | regionId: 8, 46 | regionName: 'North East LA', 47 | latitude: 34.089054958305475, 48 | longitude: -118.20459646824243, 49 | }, 50 | { 51 | regionId: 9, 52 | regionName: 'South LA 2', 53 | latitude: 33.97653096878102, 54 | longitude: -118.27194409313499, 55 | }, 56 | { 57 | regionId: 10, 58 | regionName: 'South LA 1', 59 | latitude: 34.01520365326597, 60 | longitude: -118.32558318464251, 61 | }, 62 | { 63 | regionId: 11, 64 | regionName: 'West LA', 65 | latitude: 34.03265490602317, 66 | longitude: -118.42520518385855, 67 | }, 68 | { 69 | regionId: 12, 70 | regionName: 'Harbor', 71 | latitude: 33.78488892465509, 72 | longitude: -118.27994494838939, 73 | }, 74 | ]; 75 | 76 | export default regions; 77 | -------------------------------------------------------------------------------- /src/components/layout/Main/Desktop/Export/ExportFailure.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | styled, 5 | Box, 6 | Icon, 7 | IconButton, 8 | Stack, 9 | Typography, 10 | } from '@mui/material'; 11 | import Close from '@assets/close.svg'; 12 | import Warning from '@assets/warning-error.svg'; 13 | import useStyles from './useStyles'; 14 | 15 | const StyledBox = styled(Box)(({ theme }) => ({ 16 | position: 'absolute', 17 | top: '30%', 18 | backgroundColor: '#29404F', 19 | padding: theme.spacing(3), 20 | boxShadow: theme.shadows[5], 21 | textAlign: 'center', 22 | maxWidth: '317px', 23 | maxHeight: '220px', 24 | borderRadius: '10px', 25 | })); 26 | 27 | function ExportFailure({ onClose }) { 28 | const classes = useStyles(); 29 | 30 | return ( 31 | 32 | 33 | 34 | 41 | 42 | close icon 43 | 44 | 45 | 46 | 47 | 48 | warning icon 49 | 50 | 51 | Oops! Something 52 |
    53 | went wrong. Please try 54 |
    55 | again later. 56 |
    57 |
    58 |
    59 |
    60 | ); 61 | } 62 | 63 | export default ExportFailure; 64 | 65 | ExportFailure.propTypes = { 66 | onClose: PropTypes.func.isRequired, 67 | }; 68 | -------------------------------------------------------------------------------- /src/routes/Routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Routes, Route, Navigate, useLocation, 4 | } from 'react-router-dom'; 5 | import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles'; 6 | import Paper from '@mui/material/Paper'; 7 | import Box from '@mui/material/Box'; 8 | import queryString from 'query-string'; 9 | import theme, { darkTheme } from '@theme/theme'; 10 | import Desktop from '@pages/Home/index'; 11 | import Faqs from '@pages/FAQ/Faqs'; 12 | import About from '@pages/About/About'; 13 | import Contact from '@pages/Contact/Contact'; 14 | import Privacy from '@pages/Privacy/Privacy'; 15 | import ContentBottom from '@components/layout/ContentBottom'; 16 | 17 | export default function AppRoutes() { 18 | const { pathname, search } = useLocation(); 19 | const values = queryString.parse(search); 20 | 21 | return ( 22 | <> 23 | {/* Dark Theme - Map. */} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {/* /* Default theme - Everything else. */ } 35 | 36 | 37 | 38 | 39 | } /> 40 | } /> 41 | } /> 42 | } /> 43 | } /> 44 | 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/theme/theme.js: -------------------------------------------------------------------------------- 1 | import { createTheme, responsiveFontSizes } from '@mui/material/styles'; 2 | import colors from '@theme/colors'; 3 | import gaps from '@theme/gaps'; 4 | import borderRadius from '@theme/borderRadius'; 5 | import typography from '@theme/typography'; 6 | 7 | const isLightTheme = true; 8 | 9 | const commonThemeItems = { 10 | gaps, 11 | borderRadius, 12 | typography, 13 | header: { 14 | height: '62px', 15 | }, 16 | footer: { 17 | height: '40px', 18 | }, 19 | }; 20 | 21 | // lightTheme is used by content pages - see Routes.jsx. 22 | const lightTheme = responsiveFontSizes(createTheme({ 23 | ...commonThemeItems, 24 | palette: { 25 | mode: 'light', 26 | selected: { primary: colors.selectedPrimary }, 27 | primary: { 28 | main: colors.primaryLight, 29 | }, 30 | secondary: { 31 | main: colors.primaryFocus, 32 | }, 33 | text: { 34 | primary: colors.textPrimaryLight, 35 | dark: colors.textDark, 36 | }, 37 | }, 38 | })); 39 | 40 | // darkTheme is used by map page 41 | const darkTheme = responsiveFontSizes(createTheme({ 42 | ...commonThemeItems, 43 | palette: { 44 | mode: 'dark', 45 | primary: { 46 | main: colors.primaryDarkMain, 47 | dark: colors.primaryDark, 48 | focus: colors.primaryFocus, 49 | }, 50 | selected: { primary: colors.selectedPrimary }, 51 | secondary: { 52 | main: colors.textSecondaryDark, 53 | light: colors.textSecondaryLight, 54 | }, 55 | background: { 56 | default: colors.secondaryDark, 57 | }, 58 | text: { 59 | dark: colors.textDark, 60 | cyan: colors.secondaryFocus, 61 | primaryDark: colors.textPrimaryDark, 62 | secondaryDark: colors.textSecondaryDark, 63 | secondaryLight: colors.textSecondaryLight, 64 | }, 65 | }, 66 | })); 67 | 68 | const theme = isLightTheme ? lightTheme : darkTheme; 69 | 70 | export { 71 | lightTheme, 72 | darkTheme, 73 | theme as default, 74 | }; 75 | -------------------------------------------------------------------------------- /backend/facts.js: -------------------------------------------------------------------------------- 1 | import { shuffle } from '@utils'; 2 | 3 | /* Source: https://medium.com/datala/latest */ 4 | const facts = shuffle([ 5 | 'The LA Neighborhood Council system comprises 99 Neighborhood Councils, each serving approximately 40,000 people.', 6 | 'In some cases, the boundaries of two or more Certified Neighborhood Councils may overlap if the proposed area is designed for public use, such as a park, school, library, police or fire station, or major thoroughfare; or houses a historical landmark or facility.', 7 | 'The Neighborhood Council system was launched in 1999 to ensure that the City government stays connected and adaptive to the unique needs and diverse characteristics of LA’s various communities, promoting more inclusive and representative local governance.', 8 | 'Hack for LA is a volunteer-driven civic tech organization and is a project of Civic Tech Structure, Inc.—an organization that empowers nonprofits and governments through innovative solutions, scaling, and experimentation, to drive positive change and community impact.', 9 | 'Spanning 4,084 square miles, LA County is one of the largest in the United States.', 10 | 'With nearly 10 million residents—accounting for 27% of California’s total population—LA County is the most populous of any county in the United States.', 11 | 'LA County encompasses 88 cities and over 120 unincorporated communities.', 12 | 'Neighborhood Councils are funded by taxpayer dollars, and their board members, who are elected by local communities, serve as volunteer LA City government officials.', 13 | 'Each Neighborhood Council is distinct, with its own board structure and stakeholder representation. Board sizes range from 7 to 35 members, with terms typically lasting two years.', 14 | 'Neighborhood Councils receive $32,000 annually in public funds. These funds support community events, programs, and advocacy initiatives, addressing issues such as infrastructure, public safety, and economic development.', 15 | ]); 16 | 17 | export default facts; 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Deploy NRD Frontend 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | defaults: 9 | run: 10 | working-directory: . 11 | 12 | jobs: 13 | build: 14 | permissions: 15 | contents: write 16 | 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Repo Code 20 | uses: actions/checkout@v2 21 | 22 | # - name: Setup Python 23 | # uses: actions/setup-python@v2 24 | # with: 25 | # python-version: '2.x' 26 | 27 | - name: Use Node.js 18.x 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: '18.x' 31 | 32 | - name: Install Packages 33 | run: npm ci 34 | 35 | - name: Setup environment 36 | run: | 37 | echo "VITE_MAPBOX_TOKEN=${{ secrets.VITE_MAPBOX_TOKEN }}" > .env 38 | echo "SOCRATA_API_URL=${{ secrets.SOCRATA_API_URL }}" >> .env 39 | echo "SOCRATA_TOKEN=${{ secrets.SOCRATA_TOKEN }}" >> .env 40 | echo "VITE_CONTENTFUL_SPACE=${{ secrets.VITE_CONTENTFUL_SPACE }}" >> .env 41 | echo "VITE_CONTENTFUL_TOKEN=${{ secrets.VITE_CONTENTFUL_TOKEN }}" >> .env 42 | echo "VITE_DATA_SOURCE=${{ env.VITE_DATA_SOURCE }}" >> .env 43 | 44 | # JamesIves 45 | - name: Build website 46 | run: npm run build 47 | 48 | - name: JamesIves Deploy 49 | uses: JamesIves/github-pages-deploy-action@4.1.1 50 | with: 51 | # TOKEN: ${{ secrets.ACCESS_TOKEN }} 52 | FOLDER: dist 53 | BRANCH: gh-pages 54 | # Chelsey 55 | # - name: Configuring git… 56 | # run: | 57 | # git config --global user.email "hungrylulu8@gmail.com" 58 | # git config --global user.name "edwinjue" 59 | # git remote rm origin 60 | # git remote add origin https://github.com/edwinjue/311-data-v2-gh-pages.git 61 | 62 | # - name: Chelsey Deploy via gh-pages 63 | # run: | 64 | # npm run deploy 65 | -------------------------------------------------------------------------------- /src/components/layout/Main/Desktop/Export/ExportDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | styled, 5 | Modal as BaseModal, 6 | } from '@mui/material'; 7 | import ExportConfirmation from './ExportConfirmation'; 8 | import ExportDownload from './ExportDownload'; 9 | import ExportWarning from './ExportWarning'; 10 | import ExportFailure from './ExportFailure'; 11 | 12 | const StyledModal = styled(BaseModal)({ 13 | display: 'flex', 14 | alignItems: 'center', 15 | justifyContent: 'center', 16 | }); 17 | 18 | function ExportDialog({ 19 | open, onClose, onConfirm, onConfirmationClose, errorType, requestType, dialogType, fileSize, 20 | }) { 21 | return ( 22 | 26 | <> 27 | {errorType && } 28 | {(dialogType === 'confirmation') && } 29 | {(dialogType === 'downloading') && } 30 | {(dialogType === 'success') && } 31 | {(dialogType === 'failed') && } 32 | 33 | 34 | ); 35 | } 36 | 37 | export default ExportDialog; 38 | 39 | ExportDialog.propTypes = { 40 | open: PropTypes.bool.isRequired, 41 | onClose: PropTypes.func, 42 | onConfirm: PropTypes.func, 43 | onConfirmationClose: PropTypes.func, 44 | errorType: PropTypes.string, 45 | requestType: PropTypes.string, 46 | dialogType: PropTypes.string, 47 | fileSize: PropTypes.number, 48 | }; 49 | 50 | ExportDialog.defaultProps = { 51 | onClose: undefined, 52 | onConfirm: undefined, 53 | onConfirmationClose: undefined, 54 | errorType: undefined, 55 | requestType: undefined, 56 | dialogType: undefined, 57 | fileSize: undefined, 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/layout/Main/Reports.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | import makeStyles from '@mui/styles/makeStyles'; 4 | import { Backdrop, CircularProgress } from '@mui/material'; 5 | 6 | const useStyles = makeStyles(theme => ({ 7 | root: { 8 | height: `calc(100vh - ${theme.header.height} - ${theme.footer.height})`, 9 | width: '100vw', 10 | }, 11 | backdrop: { 12 | position: 'absolute', 13 | top: theme.header.height, 14 | bottom: theme.footer.height, 15 | height: `calc(100vh - ${theme.header.height} - ${theme.footer.height})`, 16 | }, 17 | })); 18 | 19 | const REPORTS_PATH = '/reports/'; 20 | 21 | function Reports() { 22 | const [isLoading, setIsLoading] = React.useState(true); 23 | const classes = useStyles(); 24 | 25 | const url = import.meta.env.REPORT_URL; 26 | const location = useLocation(); 27 | const reportPath = location.pathname.slice(REPORTS_PATH.length - 1); 28 | const reportRef = React.useRef(reportPath); 29 | 30 | React.useEffect(() => { 31 | if (reportPath !== reportRef.current) { 32 | setIsLoading(true); 33 | } 34 | 35 | if (isLoading) { 36 | const timer = setTimeout(() => setIsLoading(false), 2000); 37 | return () => clearTimeout(timer); 38 | } 39 | reportRef.current = reportPath; 40 | 41 | return () => { 42 | // componentWillUnmount code goes here... 43 | }; 44 | }, [reportPath, isLoading]); 45 | 46 | return ( 47 |
    50 | 56 | 57 | 58 |