├── 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 |
13 | {data?.map(request => (
14 | {request.SRNumber}
15 | ))}
16 |
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 |
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 |
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 | {
28 | onSelect(option.dates);
29 | }}
30 | className={`${classes.option} ${highlightIfSelected(
31 | option.dates,
32 | dates,
33 | )}`}
34 | >
35 | {option.text}
36 |
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 |
43 |
44 |
45 |
46 |
47 |
48 |
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 |
65 |
66 | );
67 | }
68 |
69 | export default Reports;
70 |
--------------------------------------------------------------------------------
/src/features/Map/mapColors.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import { REQUEST_TYPES } from '@components/common/CONSTANTS';
4 |
5 | // carto schemes from: https://carto.com/carto-colors/
6 |
7 | const COLOR_SCHEMES = {
8 | original: Object.keys(REQUEST_TYPES).map(type => REQUEST_TYPES[type].color),
9 |
10 | // carto prism
11 | prism: [
12 | '#5F4690',
13 | '#1D6996',
14 | '#38A6A5',
15 | '#0F8554',
16 | '#73AF48',
17 | '#EDAD08',
18 | '#E17C05',
19 | '#CC503E',
20 | '#94346E',
21 | '#6F4070',
22 | '#994E95',
23 | // '#666666'
24 | ],
25 |
26 | // carto vivid
27 | vivid: [
28 | '#E58606',
29 | '#5D69B1',
30 | '#52BCA3',
31 | '#99C945',
32 | '#CC61B0',
33 | '#24796C',
34 | '#DAA51B',
35 | '#2F8AC4',
36 | '#764E9F',
37 | '#ED645A',
38 | '#CC3A8E',
39 | '#A5AA99'
40 | ],
41 |
42 | // carto bold
43 | bold: [
44 | '#7F3C8D',
45 | '#11A579',
46 | '#3969AC',
47 | '#F2B701',
48 | '#E73F74',
49 | '#80BA5A',
50 | '#E68310',
51 | '#008695',
52 | '#CF1C90',
53 | '#f97b72',
54 | '#4b4b8f',
55 | '#A5AA99'
56 | ],
57 |
58 | // carto pastel
59 | pastel: [
60 | '#66C5CC',
61 | '#F6CF71',
62 | '#F89C74',
63 | '#DCB0F2',
64 | '#87C55F',
65 | '#9EB9F3',
66 | '#FE88B1',
67 | '#C9DB74',
68 | '#8BE0A4',
69 | '#B497E7',
70 | '#D3B484',
71 | '#B3B3B3'
72 | ],
73 |
74 | club: Array.from({ length: 11 }).map((_, index) => {
75 | const hue = 170 + Math.round(190 * index / 11);
76 | return `hsl(${hue}, 100%, 50%)`;
77 | }),
78 |
79 | mono: [
80 | '#FFFFFF'
81 | ],
82 | }
83 |
84 | export const COLOR_SCHEME_NAMES = Object.keys(COLOR_SCHEMES);
85 |
86 | export function getColors(scheme) {
87 | const offset = scheme === 'prism' ? 2 : 0;
88 | const colors = COLOR_SCHEMES[scheme];
89 | return Object.keys(REQUEST_TYPES).reduce((out, type, idx) => {
90 | out[type] = colors[(idx + offset) % colors.length];
91 | return out;
92 | }, {});
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/layout/Main/Desktop/Export/ExportWarning.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | styled,
5 | Box,
6 | Button,
7 | Icon,
8 | IconButton,
9 | Stack,
10 | Typography,
11 | } from '@mui/material';
12 | import Close from '@assets/close.svg';
13 | import InfoAlert from '@assets/yellow_tips.svg';
14 | import useStyles from './useStyles';
15 |
16 | const StyledBox = styled(Box)(({ theme }) => ({
17 | position: 'absolute',
18 | top: '30%',
19 | left: '45%',
20 | backgroundColor: theme.palette.primary.main,
21 | padding: theme.spacing(4),
22 | boxShadow: theme.shadows[5],
23 | textAlign: 'center',
24 | maxWidth: '280px',
25 | maxHeight: '418px',
26 | borderRadius: '8px',
27 | whiteSpace: 'pre-wrap',
28 | }));
29 |
30 | function ExportWarning({ onClose, errorType }) {
31 | const classes = useStyles();
32 |
33 | return (
34 |
35 |
36 |
37 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | {errorType}
53 |
54 |
55 | OK
56 |
57 |
58 |
59 |
60 | );
61 | }
62 |
63 | export default ExportWarning;
64 |
65 | ExportWarning.propTypes = {
66 | onClose: PropTypes.func.isRequired,
67 | errorType: PropTypes.string,
68 | };
69 |
70 | ExportWarning.defaultProps = {
71 | errorType: undefined,
72 | };
73 |
--------------------------------------------------------------------------------
/.github/workflows/Continuous_Deployment_Frontend_Prod.yml:
--------------------------------------------------------------------------------
1 | name: Deploy_Frontend_Prod
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | paths:
8 | - 'client/**'
9 |
10 | defaults:
11 | run:
12 | working-directory: client
13 |
14 | jobs:
15 | deploy_frontend_prod:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout Repository Code
19 | uses: actions/checkout@v1
20 | - name: Setup Node
21 | uses: actions/setup-node@v3
22 | with:
23 | node-version: 12
24 | - name: Rebuild sass
25 | run: npm rebuild node-sass
26 | - name: Install Packages
27 | run: npm install
28 | - name: Setup environment
29 | run: |
30 | echo VITE_MAPBOX_TOKEN=${{ secrets.VITE_MAPBOX_TOKEN }} > .env
31 | echo MAPBOX_STREETS_URL=${{ secrets.MAPBOX_STREETS_URL }} >> .env
32 | echo MAPBOX_SATELLITE_URL=${{ secrets.MAPBOX_SATELLITE_URL }} >> .env
33 | echo API_URL=${{ secrets.API_URL_PROD }} >> .env
34 | echo REPORT_URL=${{ secrets.REPORT_URL }} >> .env
35 | echo SENTRY_CLIENT_DSN=${{ secrets.SENTRY_CLIENT_DSN }} >> .env
36 | echo MIXPANEL_ENABLED=${{ secrets.MIXPANEL_ENABLED }} >> .env
37 | echo MIXPANEL_TOKEN_PROD=${{ secrets.MIXPANEL_TOKEN_PROD }} >> .env
38 | echo MIXPANEL_TOKEN_DEV=${{ secrets.MIXPANEL_TOKEN_DEV }} >> .env
39 | echo GITHUB_SHA=${{ github.sha }} >> .env
40 | - name: Build project
41 | run: npm run build
42 | - name: Configure AWS Credentials
43 | uses: aws-actions/configure-aws-credentials@v1
44 | with:
45 | aws-access-key-id: ${{ secrets.AWS_CI_ACCESS_KEY_ID }}
46 | aws-secret-access-key: ${{ secrets.AWS_CI_SECRET_ACCESS_KEY }}
47 | aws-region: us-east-1
48 | - name: Sync Production Build To S3
49 | run: |
50 | aws s3 sync dist s3://${{ secrets.S3_BUCKET_PROD }} --follow-symlinks --delete
51 | - name: Invalidate Cloudfront Cache
52 | run: |
53 | aws cloudfront create-invalidation --distribution-id ${{ secrets.CDN_DISTRIBUTION_ID_PROD }} --paths "/*"
54 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/blank-design-issue.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Blank Design Issue
3 | about: Template for designers to create issues and prepare for engineering hand off.
4 | title: ''
5 | labels: 'Complexity: Missing, draft, Feature: Missing, Milestone: Missing, role: UI/UX
6 | Design, size: Missing'
7 | assignees: ''
8 |
9 | ---
10 |
11 | ### Overview
12 | We need to do X for Y reason.
13 |
14 | ### Action Items
15 |
16 | - [ ] Complete Design Iterations section below
17 | - [ ] design requirement 1
18 | - [ ] design requirement 2
19 | - [ ] design requirement 99
20 | - [ ] Document user interaction in Figma
21 | - [ ] Update the Hand Off section of this ticket with the final iteration of this design
22 |
23 |
24 | ---
25 |
26 | ### Design Iterations
27 |
28 | **Please move ticket between `In Progress` and `In Review` to assist PM team**
29 |
30 | Iteration 1
31 |
32 |
33 | Link to notes: `REPLACE WITH COMMENT URL`
34 |
35 | `REPLACE WITH SCREENSHOT UPLOAD`
36 |
37 |
38 |
39 |
40 | ---
41 |
42 | ### Hand Off Materials
43 |
44 | Figma Section Name: `REPLACE WITH SECTION NAME`
45 |
46 | Before Screenshot
47 |
48 |
49 | `REPLACE WITH SCREENSHOT UPLOAD`
50 |
51 |
52 |
53 |
54 | After Screenshot (Finalized)
55 |
56 |
57 | `REPLACE WITH SCREENSHOT UPLOAD`
58 |
59 |
60 |
61 |
62 | ### Designer Resources
63 |
64 | Iteration Dropdown Copy/Paste
65 |
66 |
67 | ```
68 | Iteration X
69 |
70 |
71 | Link to notes: `REPLACE WITH COMMENT URL`
72 |
73 | `REPLACE WITH SCREENSHOT UPLOAD`
74 |
75 |
76 |
77 | ```
78 |
79 |
80 |
81 |
82 | Instructions for Engineering Hand Off
83 |
84 |
85 | To Start Engineering Hand Off...
86 | 1. Ensure all Hand Off Materials are filled in
87 | 3. Add the "ready for dev lead" label
88 | 4. Leave a comment saying "This ticket is ready for engineering hand off."
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/.github/workflows/create_release_dev.yml:
--------------------------------------------------------------------------------
1 | name: Create API Image (DEV)
2 | on:
3 | push:
4 | branches:
5 | - dev
6 | paths:
7 | - "server/api/**"
8 | workflow_dispatch:
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: Get branch name
18 | uses: nelonoel/branch-name@v1.0.1
19 | - name: Setup environment
20 | run: |
21 | echo GITHUB_CODE_VERSION=${{ env.BRANCH_NAME }} >> server/api/.env
22 | echo GITHUB_SHA=${{ github.sha }} >> server/api/.env
23 | echo GITHUB_TOKEN=${{ secrets.GH_ISSUES_TOKEN }} >> server/api/.env
24 | echo GITHUB_PROJECT_URL=${{ secrets.GH_PROJECT_URL }} >> server/api/.env
25 | echo GITHUB_ISSUES_URL=https://api.github.com/repos/hackforla/311-data-support/issues >> server/api/.env
26 | echo SENDGRID_API_KEY=${{ secrets.SENDGRID_API_KEY }} >> server/api/.env
27 | echo DEBUG=True >> server/api/.env
28 | echo DB_ECHO=True >> server/api/.env
29 | echo API_ALLOWED_ORIGINS=https://dev.311-data.org,http://localhost:3000,http://0.0.0.0:3000,https://311-data.matt-webster.net >> server/api/.env
30 | - name: Build and Push Image to Docker Hub
31 | uses: docker/build-push-action@v1
32 | with:
33 | username: ${{ secrets.DOCKER_USERNAME }}
34 | password: ${{ secrets.DOCKER_PASSWORD }}
35 | path: server/api
36 | repository: la311data/311_data_api
37 | tag_with_ref: true
38 | tag_with_sha: true
39 | - name: Configure AWS Credentials
40 | uses: aws-actions/configure-aws-credentials@v1
41 | with:
42 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
43 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
44 | aws-region: ${{ secrets.AWS_REGION }}
45 | - name: (Rolling) Restart ECS Tasks
46 | run: |
47 | aws ecs update-service --cluster prod-la-311-data-cluster --service dev-la-311-data-svc --force-new-deployment
48 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import PropTypes from 'proptypes';
3 | import { HashRouter } from 'react-router-dom';
4 | import { Helmet } from 'react-helmet';
5 | import { connect } from 'react-redux';
6 | import { getMetadataRequest } from '@reducers/metadata';
7 |
8 | import Header from '@components/layout/Header';
9 | import Footer from '@components/layout/Footer';
10 | import AppRoutes from '@routes/Routes';
11 |
12 | const TITLE = '311-Data Neighborhood Engagement Tool';
13 | const DESCRIPTION =
14 | 'Hack for LA’s 311-Data Team has partnered with the Los Angeles Department of Neighborhood Empowerment and LA Neighborhood Councils to create 311 data dashboards to provide all City of LA neighborhoods with actionable information at the local level.';
15 | const URL = 'https://www.311-data.org/';
16 | const SOCIAL_IMAGE = '/social-media-card-image.png';
17 |
18 | function App({ getMetadata }) {
19 | useEffect(() => {
20 | getMetadata();
21 | });
22 |
23 | return (
24 |
25 |
26 | {TITLE}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
50 | const mapDispatchToProps = dispatch => ({
51 | getMetadata: () => dispatch(getMetadataRequest()),
52 | });
53 |
54 | export default connect(null, mapDispatchToProps)(App);
55 |
56 | App.propTypes = {
57 | getMetadata: PropTypes.func.isRequired,
58 | };
59 |
--------------------------------------------------------------------------------
/src/components/layout/Footer/LastUpdated.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | Component to show the date of the last data update in the footer which assumes data is available in conn from DbContext,
3 | which is no longer a valid assumption if using Socrata or in new codebase structure
4 | */
5 | import React, { useContext, useEffect, useState } from 'react';
6 | import moment from 'moment';
7 | import Typography from '@mui/material/Typography';
8 | import makeStyles from '@mui/styles/makeStyles';
9 | import DbContext from '@db/DbContext';
10 | import ddbh from '@utils/duckDbHelpers.js';
11 | import { isEmpty, toNonBreakingSpaces } from '@utils';
12 |
13 | const DATA_SOURCE = import.meta.env.VITE_DATA_SOURCE;
14 |
15 | const useStyles = makeStyles(theme => ({
16 | lastUpdated: {
17 | fontWeight: theme.typography.fontWeightMedium,
18 | color: theme.palette.text.dark,
19 | lineHeight: theme.footer.height,
20 | },
21 | }));
22 |
23 | function LastUpdated() {
24 | const classes = useStyles();
25 | const [lastUpdated, setLastUpdated] = useState('');
26 | const { conn } = useContext(DbContext);
27 |
28 | useEffect(() => {
29 | if (DATA_SOURCE === 'SOCRATA') {
30 | setLastUpdated(Date.now())
31 | }
32 | },[DATA_SOURCE])
33 |
34 | useEffect(() => {
35 | const getLastUpdated = async () => {
36 | const getLastUpdatedSQL = 'select max(createddate) from requests_2025;';
37 |
38 | const lastUpdatedAsArrowTable = await conn.query(getLastUpdatedSQL);
39 | const results = ddbh.getTableData(lastUpdatedAsArrowTable);
40 |
41 | if (!isEmpty(results)) {
42 | const lastUpdatedValue = results[0];
43 | setLastUpdated(lastUpdatedValue);
44 | }
45 | };
46 |
47 | if (DATA_SOURCE !== 'SOCRATA' && conn) {
48 | getLastUpdated();
49 | }
50 | }, [conn, DATA_SOURCE]);
51 |
52 | return (
53 | lastUpdated && (
54 |
55 |
56 | {toNonBreakingSpaces(
57 | `Data last updated ${moment(lastUpdated).format('MM/DD/YY')}`
58 | )}
59 |
60 |
61 | )
62 | );
63 | }
64 |
65 | export default LastUpdated;
66 |
--------------------------------------------------------------------------------
/src/pages/Blog/Blog.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactMarkdown from 'react-markdown';
3 | import {
4 | Container,
5 | Box,
6 | Grid,
7 | List,
8 | ListItem,
9 | } from '@mui/material';
10 | import makeStyles from '@mui/styles/makeStyles';
11 | import useContentful from '../../hooks/useContentful';
12 |
13 | const query = `
14 | query {
15 | blogPostCollection(order: publishDate_DESC) {
16 | items {
17 | sys { id }
18 | slug
19 | title
20 | body
21 | publishDate
22 | }
23 | }
24 | }
25 | `;
26 |
27 | const useStyles = makeStyles({
28 | root: {
29 | color: 'black',
30 | backgroundColor: 'white',
31 | padding: '2em',
32 | '& h1': {
33 | fontSize: '2.5em',
34 | },
35 | '& img': {
36 | maxWidth: '100%',
37 | height: 'auto',
38 | display: 'block',
39 | marginLeft: 'auto',
40 | marginRight: 'auto',
41 | },
42 | },
43 | });
44 |
45 | function Blog() {
46 | const { data, errors } = useContentful(query);
47 | const classes = useStyles();
48 |
49 | React.useEffect(() => {
50 | if (errors) console.log(errors);
51 | }, [errors]);
52 |
53 | if (!data) {
54 | return null;
55 | }
56 | return (
57 |
58 |
59 |
60 | { data.blogPostCollection.items.map(item => (
61 |
62 | {item.title}
63 |
64 | {new Date(item.publishDate).toLocaleDateString()}
65 |
66 | {item.body}
67 |
68 | ))}
69 |
70 |
71 |
72 | { data.blogPostCollection.items.map(item => (
73 |
74 | {item.title}
75 |
76 | ))}
77 |
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | export default Blog;
85 |
--------------------------------------------------------------------------------
/src/features/Map/LocationDetail.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import makeStyles from '@mui/styles/makeStyles';
4 | import Typography from '@mui/material/Typography';
5 | import Link from '@mui/material/Link';
6 | import Box from '@mui/material/Box';
7 |
8 | import sharedStyles from '@theme/styles';
9 |
10 | const useStyles = makeStyles(theme => ({
11 | locationInfo: {
12 | borderRadius: 10,
13 | width: 325,
14 | backgroundColor: theme.palette.primary.main,
15 | padding: 15,
16 | marginTop: theme.gaps.xs,
17 | },
18 | subheader: {
19 | ...theme.typography.body1,
20 | color: '#A8A8A8',
21 | },
22 | link: {
23 | ...theme.typography.h6,
24 | color: '#ececec',
25 | },
26 | }));
27 |
28 | function LocationDetail({
29 | address,
30 | nc,
31 | // ccs,
32 | }) {
33 | const classes = useStyles();
34 | const sharedClasses = sharedStyles();
35 |
36 | return (
37 |
38 |
43 | INFORMATION
44 |
45 | {address && (
46 |
47 | Address:
48 | {address}
49 |
50 | )}
51 | {nc && (
52 |
53 | Neighborhood Council District:
54 |
61 | {nc.councilName}
62 |
63 |
64 | )}
65 |
66 | );
67 | }
68 |
69 | export default LocationDetail;
70 |
71 | LocationDetail.propTypes = {
72 | address: PropTypes.string,
73 | nc: PropTypes.shape({
74 | website: PropTypes.string,
75 | councilName: PropTypes.string,
76 | }),
77 | };
78 |
79 | LocationDetail.defaultProps = {
80 | address: undefined,
81 | nc: null,
82 | };
83 |
--------------------------------------------------------------------------------
/.github/workflows/Continuous_Deployment_Frontend_Dev.yml:
--------------------------------------------------------------------------------
1 | name: Deploy_Frontend_Dev
2 |
3 | on:
4 | push:
5 | branches:
6 | - dev
7 | paths:
8 | - 'client/**'
9 | workflow_dispatch:
10 |
11 | defaults:
12 | run:
13 | working-directory: client
14 |
15 | jobs:
16 | deploy_frontend_dev:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout Repository Code
20 | uses: actions/checkout@v1
21 | - name: Setup Node
22 | uses: actions/setup-node@v3
23 | with:
24 | node-version: 12
25 | - name: Rebuild sass
26 | run: npm rebuild node-sass
27 | - name: Install Packages
28 | run: npm install
29 | - name: Setup environment
30 | run: |
31 | echo VITE_MAPBOX_TOKEN=${{ secrets.VITE_MAPBOX_TOKEN }} > .env
32 | echo MAPBOX_STREETS_URL=${{ secrets.MAPBOX_STREETS_URL }} >> .env
33 | echo MAPBOX_SATELLITE_URL=${{ secrets.MAPBOX_SATELLITE_URL }} >> .env
34 | echo API_URL=${{ secrets.API_URL_DEV }} >> .env
35 | echo REPORT_URL=${{ secrets.REPORT_URL }} >> .env
36 | echo SENTRY_CLIENT_DSN=${{ secrets.SENTRY_CLIENT_DSN }} >> .env
37 | echo MIXPANEL_ENABLED=${{ secrets.MIXPANEL_ENABLED }} >> .env
38 | echo MIXPANEL_TOKEN_PROD=${{ secrets.MIXPANEL_TOKEN_PROD }} >> .env
39 | echo MIXPANEL_TOKEN_DEV=${{ secrets.MIXPANEL_TOKEN_DEV }} >> .env
40 | echo VITE_CONTENTFUL_SPACE=${{ secrets.VITE_CONTENTFUL_SPACE }} >> .env
41 | echo VITE_CONTENTFUL_TOKEN=${{ secrets.VITE_CONTENTFUL_TOKEN }} >> .env
42 | echo GITHUB_SHA=${{ github.sha }} >> .env
43 | - name: Build project
44 | run: npm run build
45 | - name: Configure AWS Credentials
46 | uses: aws-actions/configure-aws-credentials@v1
47 | with:
48 | aws-access-key-id: ${{ secrets.AWS_CI_ACCESS_KEY_ID }}
49 | aws-secret-access-key: ${{ secrets.AWS_CI_SECRET_ACCESS_KEY }}
50 | aws-region: us-east-1
51 | - name: Sync Development Build To S3
52 | run: |
53 | aws s3 sync dist s3://${{ secrets.S3_BUCKET_DEV }} --follow-symlinks --delete
54 | - name: Invalidate Cloudfront Cache
55 | run: |
56 | aws cloudfront create-invalidation --distribution-id ${{ secrets.CDN_DISTRIBUTION_ID_DEV }} --paths "/*"
57 |
--------------------------------------------------------------------------------
/.github/workflows/create_release_dev_v1.yml:
--------------------------------------------------------------------------------
1 | # This Action is specifically for creating and deploying a new API Image from the 'dev' branch.
2 | # The API image is used only by the prod server that serves the v1 frontend.
3 | # It differs from create_release_dev.yml in that its configured for prod usage.
4 | name: Create API Image (DEV) (v1)
5 | on:
6 | push:
7 | branches:
8 | - dev
9 | paths:
10 | - "server/api/**"
11 | workflow_dispatch:
12 |
13 | jobs:
14 | build:
15 | name: Create Docker Image
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout code
19 | uses: actions/checkout@v2
20 | - name: Get branch name
21 | uses: nelonoel/branch-name@v1.0.1
22 | - name: Setup environment
23 | run: |
24 | echo GITHUB_CODE_VERSION=${{ env.BRANCH_NAME }} >> server/api/.env
25 | echo GITHUB_SHA=${{ github.sha }} >> server/api/.env
26 | echo GITHUB_TOKEN=${{ secrets.GH_ISSUES_TOKEN }} >> server/api/.env
27 | echo GITHUB_PROJECT_URL=${{ secrets.GH_PROJECT_URL }} >> server/api/.env
28 | echo GITHUB_ISSUES_URL=https://api.github.com/repos/hackforla/311-data-support/issues >> server/api/.env
29 | echo SENDGRID_API_KEY=${{ secrets.SENDGRID_API_KEY }} >> server/api/.env
30 | echo API_ALLOWED_ORIGINS=https://311-data.org,https://www.311-data.org >> server/api/.env
31 | - name: Build and Push Image to Docker Hub
32 | uses: docker/build-push-action@v1
33 | with:
34 | username: ${{ secrets.DOCKER_USERNAME }}
35 | password: ${{ secrets.DOCKER_PASSWORD }}
36 | path: server/api
37 | repository: la311data/311_data_api
38 | tag_with_sha: true
39 | tags: latest # Tag with 'latest' since the prod service uses the 'latest' image.
40 |
41 | - name: Configure AWS Credentials
42 | uses: aws-actions/configure-aws-credentials@v1
43 | with:
44 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
45 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
46 | aws-region: ${{ secrets.AWS_REGION }}
47 | - name: (Rolling) Restart ECS Tasks
48 | run: |
49 | aws ecs update-service --cluster prod-la-311-data-cluster --service prod-la-311-data-svc --force-new-deployment
50 |
--------------------------------------------------------------------------------
/backend/agencies.js:
--------------------------------------------------------------------------------
1 | const agencies = [
2 | {
3 | agencyId: 1,
4 | agencyName: 'Street Lighting Bureau',
5 | socrataOwner: 'BSL',
6 | website: 'http://bsl.lacity.org/',
7 | twitter: 'LAlight',
8 | },
9 | {
10 | agencyId: 2,
11 | agencyName: 'Sanitation Bureau',
12 | socrataOwner: 'LASAN',
13 | website: 'http://lacitysan.org/',
14 | twitter: 'lacitysan',
15 | },
16 | {
17 | agencyId: 3,
18 | agencyName: 'Sanitation Bureau',
19 | socrataOwner: 'LASAN',
20 | website: 'http://lacitysan.org/',
21 | twitter: 'lacitysan',
22 | },
23 | {
24 | agencyId: 4,
25 | agencyName: 'Office of Community Beautification',
26 | socrataOwner: 'OCB',
27 | website: 'http://www.laocb.org/',
28 | twitter: 'LA_OCB',
29 | },
30 | {
31 | agencyId: 5,
32 | agencyName: 'Information Technology Agency',
33 | socrataOwner: 'ITA',
34 | website: 'http://ita.lacity.org/',
35 | twitter: '',
36 | },
37 | {
38 | agencyId: 6,
39 | agencyName: 'Department of Water & Power',
40 | socrataOwner: 'LADWP',
41 | website: 'https://www.ladwp.com/',
42 | twitter: 'LADWP',
43 | },
44 | {
45 | agencyId: 7,
46 | agencyName: 'Bureau of Street Services',
47 | socrataOwner: 'BSS',
48 | website: 'https://streetsla.lacity.org/',
49 | twitter: 'BSSLosAngeles',
50 | },
51 | {
52 | agencyId: 8,
53 | agencyName: 'Transportation Department',
54 | socrataOwner: 'LADOT',
55 | website: 'http://ladot.lacity.org/',
56 | twitter: 'LADOTofficial',
57 | },
58 | {
59 | agencyId: 9,
60 | agencyName: 'Department of Animal Services',
61 | socrataOwner: 'LAAS',
62 | website: 'http://www.laanimalservices.com/',
63 | twitter: 'lacitypets',
64 | },
65 | {
66 | agencyId: 10,
67 | agencyName: 'Recreation & Parks',
68 | socrataOwner: 'RAP',
69 | website: 'http://www.laparks.org/',
70 | twitter: 'LACityParks',
71 | },
72 | {
73 | agencyId: 11,
74 | agencyName: 'Engineering Bureau',
75 | socrataOwner: 'ENG',
76 | website: 'http://eng.lacity.org/',
77 | twitter: '',
78 | },
79 | {
80 | agencyId: 12,
81 | agencyName: 'Department of Building and Safety',
82 | socrataOwner: 'LADBS',
83 | website: 'https://www.ladbs.org/',
84 | twitter: '',
85 | },
86 | ];
87 |
88 | export default agencies;
89 |
--------------------------------------------------------------------------------
/src/components/layout/Main/Desktop/Export/ExportDownload.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | styled,
5 | Box,
6 | Icon,
7 | Stack,
8 | Typography,
9 | } from '@mui/material';
10 | import DownloadCircle from '@assets/download-circle.svg';
11 | import CheckCircle from '@assets/round-check-circle.svg';
12 | import LoadingDots from '@assets/loading dots.svg';
13 | import useStyles from './useStyles';
14 |
15 | const StyledBox = styled(Box)(({ theme }) => ({
16 | position: 'absolute',
17 | top: '80px',
18 | backgroundColor: theme.palette.primary.main,
19 | boxShadow: theme.shadows[5],
20 | textAlign: 'center',
21 | maxWidth: '450px',
22 | maxHeight: '65px',
23 | borderRadius: '8px',
24 | }));
25 |
26 | function ExportDownload({ dialogType }) {
27 | const downloadingMessage = 'Data downloading';
28 | const downloadSuccessMessage = 'Data exported successfully!';
29 | const classes = useStyles();
30 | const { imageIcon } = classes;
31 |
32 | return (
33 |
34 |
35 | {(dialogType === 'downloading') && (
36 | <>
37 |
38 |
39 |
40 |
41 | {downloadingMessage}
42 |
43 |
48 |
49 |
50 | >
51 | )}
52 | {(dialogType === 'success') && (
53 | <>
54 |
55 |
56 |
57 |
58 | {downloadSuccessMessage}
59 |
60 | >
61 | )}
62 |
63 |
64 | );
65 | }
66 |
67 | export default ExportDownload;
68 |
69 | ExportDownload.propTypes = {
70 | dialogType: PropTypes.string.isRequired,
71 | };
72 |
--------------------------------------------------------------------------------
/src/components/layout/Main/Desktop/ShareableLinkCreator.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import Button from '@mui/material/Button';
4 | import PropTypes from 'prop-types';
5 |
6 | function ShareableLinkCreator({
7 | requestStatus,
8 | }) {
9 | return (
10 | {
13 | // const url = new URL(`${import.meta.env.API_URL}/map`);
14 | const url = new URL(`${window.location.href.split('?')[0]}`);
15 | if (requestStatus.councilId) {
16 | url.searchParams.append('councilId', requestStatus.councilId);
17 | }
18 | for (let requestTypeIndex = 1; requestTypeIndex < 13; requestTypeIndex += 1) {
19 | if (requestStatus.requestTypes[requestTypeIndex] === false) {
20 | url.searchParams.append(`rtId${requestTypeIndex}`, requestStatus.requestTypes[requestTypeIndex]);
21 | }
22 | }
23 | url.searchParams.append('requestStatusOpen', requestStatus.requestStatus.open);
24 | url.searchParams.append('requestStatusClosed', requestStatus.requestStatus.closed);
25 | url.searchParams.append('startDate', requestStatus.startDate);
26 | url.searchParams.append('endDate', requestStatus.endDate);
27 | navigator.clipboard.writeText(url);
28 | }}
29 | >
30 | Get Shareable Link
31 |
32 | );
33 | }
34 |
35 | const mapStateToProps = state => ({
36 | requestStatus: state.filters,
37 | });
38 |
39 | export default connect(
40 | mapStateToProps,
41 | )(ShareableLinkCreator);
42 |
43 | ShareableLinkCreator.propTypes = {
44 | requestStatus: PropTypes.shape({
45 | requestStatus: PropTypes.shape({
46 | open: PropTypes.bool.isRequired,
47 | closed: PropTypes.bool.isRequired,
48 | }).isRequired,
49 | startDate: PropTypes.string,
50 | endDate: PropTypes.string,
51 | councilId: PropTypes.number,
52 | requestTypes: PropTypes.shape({
53 | 1: PropTypes.bool,
54 | 2: PropTypes.bool,
55 | 3: PropTypes.bool,
56 | 4: PropTypes.bool,
57 | 5: PropTypes.bool,
58 | 6: PropTypes.bool,
59 | 7: PropTypes.bool,
60 | 8: PropTypes.bool,
61 | 9: PropTypes.bool,
62 | 10: PropTypes.bool,
63 | 11: PropTypes.bool,
64 | 12: PropTypes.bool,
65 | }),
66 | }).isRequired,
67 | };
68 |
--------------------------------------------------------------------------------
/src/components/layout/Main/Desktop/CouncilSelector/CouncilsList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import makeStyles from '@mui/styles/makeStyles';
4 | import SearchBar from '@components/common/SearchBar';
5 | import GroupedMultiSelect from '@components/common/MultiSelect/GroupedMultiSelect';
6 |
7 | const useStyles = makeStyles(theme => ({
8 | searchWrapper: {
9 | position: 'relative',
10 | height: '50px',
11 | },
12 | search: {
13 | position: 'absolute',
14 | top: '50%',
15 | transform: 'translateY(-50%)',
16 | width: '100%',
17 | paddingLeft: theme.gaps.xs,
18 | paddingRight: theme.gaps.xs,
19 | paddingBottom: theme.gaps.sm,
20 | },
21 | scrollWrapper: {
22 | maxHeight: '300px',
23 | overflowY: 'auto',
24 | marginRight: '-10px',
25 | marginBottom: '-10px',
26 | scrollbarColor: '#616161 #818181',
27 | scrollbarWidth: 'thin',
28 | '&::-webkit-scrollbar': {
29 | width: '8px',
30 | },
31 | '&::-webkit-scrollbar-track': {
32 | background: '#818181',
33 | borderRadius: '0 0 5px 0',
34 | },
35 | '&::-webkit-scrollbar-thumb': {
36 | backgroundColor: '#616161',
37 | borderRadius: 20,
38 | height: 50,
39 | },
40 | },
41 | header: {
42 | color: theme.palette.text.cyan,
43 | marginTop: theme.gaps.xs,
44 | },
45 | }));
46 |
47 | function CouncilsList({ items, onClick, searchTerm, setSearchTerm }) {
48 | const classes = useStyles();
49 |
50 | return (
51 | <>
52 |
61 |
62 |
68 |
69 | >
70 | );
71 | }
72 |
73 | export default CouncilsList;
74 |
75 | CouncilsList.propTypes = {
76 | items: PropTypes.arrayOf(PropTypes.shape({})),
77 | onClick: PropTypes.func.isRequired,
78 | searchTerm: PropTypes.string.isRequired,
79 | setSearchTerm: PropTypes.func.isRequired
80 | };
81 |
82 | CouncilsList.defaultProps = {
83 | items: [],
84 | };
85 |
--------------------------------------------------------------------------------
/src/features/Map/geoUtils.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import {
4 | circle,
5 | mask,
6 | bbox,
7 | point,
8 | polygon,
9 | pointsWithinPolygon,
10 | booleanPointInPolygon,
11 | multiPolygon,
12 | } from '@turf/turf';
13 |
14 | import ncGeojson from '@data/ncGeojson';
15 |
16 | import { isEmpty } from '@utils';
17 |
18 | export function emptyGeo() {
19 | return {
20 | type: 'FeatureCollection',
21 | features: [],
22 | };
23 | }
24 |
25 | // removes holes in a MultiPolygon
26 | export function removeGeoHoles(feature) {
27 | if (feature.geometry.type === 'MultiPolygon')
28 | return {
29 | ...feature,
30 | geometry: {
31 | ...feature.geometry,
32 | coordinates: feature.geometry.coordinates.map((poly) => [poly[0]]),
33 | },
34 | };
35 | else return feature;
36 | }
37 |
38 | export function makeGeoCircle(center, radius = 1, opts = { units: 'miles' }) {
39 | return circle([center.lng, center.lat], radius, opts);
40 | }
41 |
42 | export function makeGeoMask(poly) {
43 | return mask(poly);
44 | }
45 |
46 | export function boundingBox(geo) {
47 | return bbox(geo);
48 | }
49 |
50 | export function pointsWithinGeo(points, geo) {
51 | return pointsWithinPolygon(points, geo);
52 | }
53 |
54 | export function isPointWithinGeo(point, geo) {
55 | return booleanPointInPolygon(point, geo);
56 | }
57 |
58 | export function getNcByLngLatv2({
59 | longitude = undefined,
60 | latitude = undefined,
61 | }) {
62 | try {
63 | if (isEmpty(longitude) || isEmpty(latitude)) {
64 | throw new Error(
65 | `longitude: ${longitude} and latitude: ${latitude} must be defined`
66 | );
67 | }
68 | console.log({ longitude, latitude });
69 | const features = ncGeojson.features;
70 | console.log({ features });
71 |
72 | let foundNcGeoObj;
73 |
74 | for (const feature of features) {
75 | let featurePolygon;
76 | if (feature.geometry.type == 'MultiPolygon')
77 | featurePolygon = multiPolygon(feature.geometry.coordinates);
78 | else {
79 | featurePolygon = polygon(feature.geometry.coordinates);
80 | }
81 |
82 | if (booleanPointInPolygon(point([longitude, latitude]), featurePolygon)) {
83 | foundNcGeoObj = feature
84 | }
85 | }
86 | console.log({ foundNcGeoObj });
87 |
88 | return foundNcGeoObj?.properties?.NC_ID;
89 | } catch (e) {
90 | console.error('In getNcByLngLatv2: Error occured: ', e);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/Dashboards/DashboardOverview.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import React, { useState, useEffect, useContext } from 'react';
3 | import { getDbRequest, getDbRequestSuccess } from '@reducers/data';
4 | import PropTypes from 'proptypes';
5 | import { useSelector, useDispatch } from 'react-redux';
6 | import ContentBody from '@components/common/ContentBody';
7 | import QuadLayout from '@dashboards/layouts/QuadLayout';
8 |
9 | import ddbh from '@utils/duckDbHelpers.js';
10 | import DbContext from '@db/DbContext';
11 |
12 | /* Ideally these Quadrants should be imported from widgets as standalone React components */
13 | import TotalByDayOfWeek from '@dashboards/widgets/TotalByDayOfWeek';
14 |
15 | const Quadrant2 = ({ data }) => Total Requests by Source
;
16 | const Quadrant3 = ({ data }) => (
17 | Median Days to Close Tickets by Request
18 | );
19 | const Quadrant4 = ({ data }) => Division Fulfilling Requests
;
20 |
21 | const DashboardOverview = () => {
22 | const dispatch = useDispatch();
23 |
24 | const [requestsData, setRequestsData] = useState([]);
25 |
26 | // TODO: Need isDataLoading state to indicate whether duckDb data is still loading
27 | const isDbLoading = useSelector((state) => state.data.isDbLoading);
28 |
29 | const { conn } = useContext(DbContext);
30 |
31 | useEffect(() => {
32 | /* Here is some boilerplate code to fetch data from duckdb */
33 | async function fetchRequests() {
34 | dispatch(getDbRequest());
35 | const requestsAsArrowTable = await conn.query(
36 | 'select * from requests limit 10'
37 | );
38 | const requests = ddbh.getTableData(requestsAsArrowTable);
39 |
40 | setRequestsData(requests);
41 | }
42 |
43 | if (!isDbLoading) {
44 | fetchRequests();
45 | dispatch(getDbRequestSuccess());
46 | }
47 | }, [isDbLoading]);
48 |
49 | if (isDbLoading) return null;
50 |
51 | return (
52 |
53 | }
55 | quadrant2={ }
56 | quadrant3={ }
57 | quadrant4={ }
58 | />
59 |
60 | );
61 | };
62 |
63 | DashboardOverview.propTypes = {
64 | data: PropTypes.arrayOf(PropTypes.shape({})),
65 | };
66 |
67 | DashboardOverview.defaultProps = {
68 | data: [{}],
69 | };
70 |
71 | export default DashboardOverview;
72 |
--------------------------------------------------------------------------------
/src/components/layout/Main/Research.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactMarkdown from 'react-markdown';
3 | import {
4 | Container, Box, Grid, List, ListItem,
5 | } from '@mui/material';
6 | import makeStyles from '@mui/styles/makeStyles';
7 | import TextHeading from '@components/common/TextHeading';
8 | import useContentful from '../../hooks/useContentful';
9 |
10 | const query = `
11 | query {
12 | blogPostCollection(order: publishDate_DESC) {
13 | items {
14 | sys { id }
15 | slug
16 | title
17 | body
18 | publishDate
19 | }
20 | }
21 | }
22 | `;
23 |
24 | const useStyles = makeStyles({
25 | root: {
26 | color: 'black',
27 | backgroundColor: 'white',
28 | padding: '2em',
29 | '& h1': {
30 | fontSize: '2.5em',
31 | },
32 | '& img': {
33 | maxWidth: '100%',
34 | height: 'auto',
35 | display: 'block',
36 | marginLeft: 'auto',
37 | marginRight: 'auto',
38 | },
39 | },
40 | });
41 |
42 | function Research() {
43 | const { data, errors } = useContentful(query);
44 | const classes = useStyles();
45 |
46 | React.useEffect(() => {
47 | if (errors) console.log(errors);
48 | }, [errors]);
49 |
50 | return (
51 | <>
52 |
53 | Research
54 |
55 | { data
56 | && (
57 |
58 |
59 |
60 | { data.blogPostCollection.items.map(item => (
61 |
62 | {item.title}
63 |
64 | {new Date(item.publishDate).toLocaleDateString()}
65 |
66 | {item.body}
67 |
68 | ))}
69 |
70 |
71 |
72 | { data.blogPostCollection.items.map(item => (
73 |
74 | {item.title}
75 |
76 | ))}
77 |
78 |
79 |
80 |
81 | )}
82 | >
83 | );
84 | }
85 |
86 | export default Research;
87 |
--------------------------------------------------------------------------------
/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/common/MultiSelect/GroupedMultiSelect.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import SelectGroup from './SelectGroup';
4 |
5 | function GroupedMultiSelect({
6 | items, onChange, groupBy, searchTerm,
7 | }) {
8 | const [filtered, setFiltered] = useState(false);
9 | const [filteredItems, setFilteredItems] = useState([]);
10 |
11 | const groups = items.reduce(
12 | (acc, item) => ({
13 | ...acc,
14 | [item[groupBy]]: [...(acc[item[groupBy]] || []), item],
15 | }),
16 | {},
17 | );
18 |
19 | useEffect(() => {
20 | if (searchTerm) {
21 | setFiltered(true);
22 | } else {
23 | setFiltered(false);
24 | }
25 | }, [searchTerm]);
26 |
27 | useEffect(() => {
28 | if (searchTerm) {
29 | const searchFilter = new RegExp(searchTerm, 'i');
30 | const filteredGroups = Object.keys(groups).reduce((acc, group) => {
31 | const filteredGroup = groups[group].filter(item => searchFilter.test(item.TOOLTIP));
32 | if (filteredGroup.length) {
33 | return { ...acc, [group]: filteredGroup };
34 | }
35 | return acc;
36 | }, {});
37 | setFilteredItems(filteredGroups);
38 | }
39 | // eslint-disable-next-line
40 | }, [searchTerm]);
41 |
42 | const sortByRegionNumber = (a, b) => {
43 | // from @data/councils.js, a and b is expected to look like this:
44 | // 'REGION 11 - WEST LA'
45 | //
46 | // so we want to get the region number and sort by it
47 |
48 | // eslint-disable-next-line no-unused-vars
49 | const [labelA, regionA, otherA] = a.split(' ');
50 | // eslint-disable-next-line no-unused-vars
51 | const [labelB, regionB, otherB] = b.split(' ');
52 |
53 | return Number(regionA) - Number(regionB);
54 | };
55 |
56 | return Object.keys(filtered ? filteredItems : groups)
57 | .sort(sortByRegionNumber)
58 | .map(name => (
59 |
65 | ));
66 | }
67 |
68 | export default GroupedMultiSelect;
69 |
70 | GroupedMultiSelect.propTypes = {
71 | items: PropTypes.arrayOf(
72 | PropTypes.shape({
73 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
74 | name: PropTypes.string,
75 | selected: PropTypes.bool,
76 | }),
77 | ),
78 | onChange: PropTypes.func,
79 | groupBy: PropTypes.string.isRequired,
80 | };
81 |
82 | GroupedMultiSelect.defaultProps = {
83 | items: [],
84 | onChange: () => null,
85 | };
86 |
--------------------------------------------------------------------------------
/src/features/Map/controls/RequestsDonut.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | import React from 'react';
4 | import PropTypes from 'proptypes';
5 | import ChartJS from 'chart.js';
6 | import { REQUEST_TYPES } from '@components/common/CONSTANTS';
7 | import { getColors } from '../mapColors';
8 |
9 | class RequestsDonut extends React.Component {
10 | canvasRef = React.createRef();
11 |
12 | getRequestCounts = selectedRequests => {
13 | return Object.keys(REQUEST_TYPES).map(t => selectedRequests[t] || 0);
14 | };
15 |
16 | getSchemeColors = colorScheme => {
17 | const colors = getColors(colorScheme);
18 | return Object.keys(REQUEST_TYPES).map(t => colors[t]);
19 | };
20 |
21 | getLabels = () => {
22 | return Object.keys(REQUEST_TYPES).map(t => REQUEST_TYPES[t].displayName);
23 | };
24 |
25 | componentDidMount() {
26 | const { selectedRequests, colorScheme } = this.props;
27 | const ctx = this.canvasRef.current.getContext('2d');
28 | this.chart = new ChartJS(ctx, {
29 | type: 'doughnut',
30 | aspectRatio: 1.0,
31 | data: {
32 | datasets: [{
33 | data: this.getRequestCounts(selectedRequests),
34 | backgroundColor: this.getSchemeColors(colorScheme),
35 | borderWidth: 0,
36 | }],
37 | labels: this.getLabels(),
38 | },
39 | options: {
40 | cutoutPercentage: 50,
41 | legend: false,
42 | title: {
43 | display: false
44 | },
45 | animation: {
46 | animateScale: false,
47 | animateRotate: true,
48 | easing: 'easeOutQuart',
49 | duration: 1000,
50 | },
51 | plugins: {
52 | chartArea: {
53 | chartBgColor: '#27272b',
54 | },
55 | },
56 | },
57 | });
58 | }
59 |
60 | componentDidUpdate(prevProps) {
61 | if (prevProps.selectedRequests !== this.props.selectedRequests) {
62 | this.chart.config.data.datasets[0].data = (
63 | this.getRequestCounts(this.props.selectedRequests)
64 | );
65 | this.chart.update();
66 | } else if (prevProps.colorScheme !== this.props.colorScheme) {
67 | this.chart.config.data.datasets[0].backgroundColor = (
68 | this.getSchemeColors(this.props.colorScheme)
69 | );
70 | this.chart.update();
71 | }
72 | }
73 |
74 | render() {
75 | return (
76 |
77 |
78 |
79 | )
80 | }
81 | }
82 |
83 | RequestsDonut.propTypes = {
84 | selectedRequests: PropTypes.shape({}),
85 | colorScheme: PropTypes.string.isRequired,
86 | };
87 |
88 | RequestsDonut.defaultProps = {
89 | selectedRequests: {},
90 | };
91 |
92 | export default RequestsDonut;
93 |
--------------------------------------------------------------------------------
/src/components/common/ChipList/StyledChip.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import makeStyles from '@mui/styles/makeStyles';
4 | import Chip from '@mui/material/Chip';
5 | import CloseIcon from '@mui/icons-material/Close';
6 | import { useTheme } from '@mui/styles/';
7 |
8 | const useStylesSolid = makeStyles(theme => ({
9 | root: {
10 | backgroundColor: props => props.color,
11 | height: 'auto',
12 | '& .MuiChip-label': {
13 | whiteSpace: 'normal',
14 | },
15 | },
16 | label: {
17 | fontFamily: 'Roboto',
18 | color: theme.palette.secondary.light,
19 | },
20 | deleteIcon: {
21 | color: theme.palette.secondary.light,
22 | },
23 | }));
24 |
25 | const useStylesOutlined = makeStyles(theme => ({
26 | root: {
27 | backgroundColor: theme.palette.secondary.light,
28 | height: 'auto',
29 | '& .MuiChip-label': {
30 | whiteSpace: 'normal',
31 | },
32 | },
33 | outlined: {
34 | borderColor: props => props.color,
35 | },
36 | label: {
37 | fontFamily: 'Roboto',
38 | color: theme.palette.text.primaryDark,
39 | },
40 | deleteIcon: {
41 | color: theme.palette.text.primaryDark,
42 | '&:hover': {
43 | color: theme.palette.secondary.main,
44 | },
45 | },
46 | }));
47 |
48 | function StyledChip({
49 | label,
50 | value,
51 | color,
52 | onDelete,
53 | outlined,
54 | sx,
55 | }) {
56 | const theme = useTheme();
57 | const classesSolid = useStylesSolid({ color });
58 | const classesOutlined = useStylesOutlined({ color });
59 |
60 | return (
61 |
72 | )}
73 | size="small"
74 | variant={outlined ? 'outlined' : 'default'}
75 | clickable={false}
76 | sx={sx}
77 | />
78 | );
79 | }
80 |
81 | export default StyledChip;
82 |
83 | StyledChip.propTypes = {
84 | label: PropTypes.oneOfType([
85 | PropTypes.string,
86 | PropTypes.object,
87 | ]).isRequired,
88 | value: PropTypes.oneOfType([
89 | PropTypes.string,
90 | PropTypes.number,
91 | ]),
92 | color: PropTypes.string,
93 | onDelete: PropTypes.func,
94 | outlined: PropTypes.bool,
95 | sx: PropTypes.shape({}),
96 | };
97 |
98 | StyledChip.defaultProps = {
99 | value: undefined,
100 | color: undefined,
101 | onDelete: undefined,
102 | outlined: false,
103 | sx: undefined,
104 | };
105 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "311-data",
3 | "version": "0.1.0",
4 | "private": false,
5 | "engines": {
6 | "node": "18.x"
7 | },
8 | "type": "module",
9 | "homepage": "https://edwinjue.github.io/311-data-v2-gh-pages",
10 | "scripts": {
11 | "predeploy": "npm run build",
12 | "deploy": "gh-pages -b gh-pages -d dist",
13 | "start": "vite",
14 | "setup": "npm install && npm run check-env",
15 | "build": "vite build",
16 | "serve": "vite preview",
17 | "lint": "eslint \"./**/*.js*\"",
18 | "lint:fix": "eslint --fix \"./**/*.js*\"",
19 | "test": "vitest",
20 | "check-env": "node ./utils/checkEnv"
21 | },
22 | "husky": {
23 | "hooks": {
24 | "pre-commit": "npm run lint"
25 | }
26 | },
27 | "dependencies": {
28 | "@duckdb/duckdb-wasm": "^1.27.0",
29 | "@emotion/react": "^11.11.1",
30 | "@emotion/styled": "^11.11.0",
31 | "@mapbox/mapbox-gl-geocoder": "^5.0.1",
32 | "@mui/icons-material": "^5.14.15",
33 | "@mui/lab": "^5.0.0-alpha.150",
34 | "@mui/material": "^5.14.15",
35 | "@mui/styles": "^5.14.15",
36 | "@mui/system": "^5.15.14",
37 | "@redux-devtools/extension": "^3.2.5",
38 | "@turf/turf": "^6.5.0",
39 | "apache-arrow": "^13.0.0",
40 | "axios": "^1.5.1",
41 | "chart.js": "^4.4.0",
42 | "classnames": "^2.3.2",
43 | "clsx": "^2.0.0",
44 | "core-js": "^3.33.1",
45 | "dataframe-js": "^1.4.4",
46 | "dotenv": "^16.3.1",
47 | "file-saver": "^2.0.5",
48 | "jszip": "^3.10.1",
49 | "lodash.debounce": "^4.0.8",
50 | "mapbox-gl": "^2.15.0",
51 | "mixpanel-browser": "^2.47.0",
52 | "moment": "^2.29.4",
53 | "papaparse": "^5.4.1",
54 | "prop-types": "^15.8.1",
55 | "proptypes": "^1.1.0",
56 | "query-string": "^8.1.0",
57 | "react": "^17.0.2",
58 | "react-day-picker": "^7.4.8",
59 | "react-dom": "^17.0.2",
60 | "react-helmet": "^6.1.0",
61 | "react-markdown": "^8.0.7",
62 | "react-redux": "^8.1.3",
63 | "react-router-dom": "^6.17.0",
64 | "react-toastify": "^9.1.3",
65 | "redux": "^4.2.1",
66 | "redux-logger": "^3.0.6",
67 | "redux-saga": "^1.2.3",
68 | "regenerator-runtime": "^0.14.0",
69 | "web-worker": "^1.2.0",
70 | "yup": "^1.6.1"
71 | },
72 | "devDependencies": {
73 | "@testing-library/react": "^12.1.5",
74 | "@vitejs/plugin-react": "^4.3.1",
75 | "eslint": "^8.52.0",
76 | "eslint-config-airbnb": "^19.0.4",
77 | "eslint-import-resolver-node": "^0.3.9",
78 | "eslint-plugin-import": "^2.29.0",
79 | "eslint-plugin-jsx-a11y": "^6.7.1",
80 | "eslint-plugin-react": "^7.33.2",
81 | "eslint-plugin-react-hooks": "^4.6.0",
82 | "gh-pages": "^6.0.0",
83 | "husky": "^8.0.3",
84 | "vite": "^5.4.2",
85 | "vitest": "^2.1.5"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/components/common/ToggleGroup.jsx:
--------------------------------------------------------------------------------
1 | import makeStyles from '@mui/styles/makeStyles';
2 | import React from 'react';
3 | import PropTypes from 'prop-types';
4 |
5 | export default function ToggleGroup({
6 | children, onToggle, value, rounded,
7 | }) {
8 | const [firstChild] = children;
9 | const [selectedValue, setSelectedValue] = React.useState(
10 | value || firstChild.props.value,
11 | );
12 |
13 | const useStyles = makeStyles(theme => {
14 | const borderRadius = {
15 | borderRadius: rounded ? theme.borderRadius.lg : theme.borderRadius.sm,
16 | };
17 |
18 | return {
19 | root: {
20 | backgroundColor: theme.palette.primary.dark,
21 | padding: theme.gaps.xs,
22 | display: 'inline-flex',
23 | alignItems: 'center',
24 | ...borderRadius,
25 | },
26 | regular: {
27 | ...borderRadius,
28 | ...theme.typography.body1,
29 | padding: theme.gaps.sm,
30 | backgroundColor: theme.palette.primary.dark,
31 | height: rounded ? 25 : 'auto',
32 | color: theme.palette.text.secondaryLight,
33 | },
34 | selected: {
35 | backgroundColor: theme.palette.primary.main,
36 | },
37 | };
38 | });
39 |
40 | const classes = useStyles();
41 |
42 | const addClasses = component => {
43 | const { props } = component;
44 |
45 | const isTheSelectedChild = props.value === selectedValue;
46 |
47 | let className = '';
48 |
49 | if (isTheSelectedChild) {
50 | className = `${classes.selected} ${classes.regular}`;
51 | } else {
52 | className = classes.regular;
53 | }
54 |
55 | const clone = React.cloneElement(component, { className });
56 |
57 | return clone;
58 | };
59 |
60 | const handleClick = e => {
61 | const isContainerClicked = e.target.classList.contains(classes.root);
62 |
63 | if (isContainerClicked) return;
64 |
65 | let element = e.target;
66 | let isNotDirectChildOfContainer = !element.parentElement.classList.contains(classes.root);
67 | while (isNotDirectChildOfContainer) {
68 | element = element.parentElement;
69 | isNotDirectChildOfContainer = !element.parentElement.classList.contains(classes.root);
70 | }
71 |
72 | setSelectedValue(element.value);
73 | if (onToggle) onToggle(element.value);
74 | };
75 |
76 | return (
77 |
78 | {children.map(component => addClasses(component))}
79 |
80 | );
81 | }
82 |
83 | ToggleGroup.propTypes = {
84 | onToggle: PropTypes.func,
85 | rounded: PropTypes.bool,
86 | value: PropTypes.string,
87 | children: PropTypes.arrayOf(PropTypes.node).isRequired,
88 | };
89 |
90 | ToggleGroup.defaultProps = {
91 | rounded: false,
92 | value: '',
93 | onToggle: undefined,
94 | };
95 |
--------------------------------------------------------------------------------
/src/utils/checkEnv.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | // Checks to see if .env has all keys in .example.env. Any missing keys will be copied.
4 | // If no .env file is found, one is created from .example.env.
5 |
6 | import fs from 'fs';
7 | import path from 'path';
8 | import { fileURLToPath } from 'url';
9 | import dotenv from 'dotenv';
10 |
11 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
12 | const red = '\x1b[31m%s\x1b[0m';
13 | const green = '\x1b[32m%s\x1b[0m';
14 | const vitePrefix = 'VITE_';
15 |
16 | const envPath = path.resolve(__dirname, '../.env');
17 | const exampleEnvPath = path.resolve(__dirname, '../.example.env');
18 |
19 | function getEnv(fileName) {
20 | return dotenv.parse(fs.readFileSync(fileName));
21 | }
22 |
23 | (function checkEnv() {
24 | console.log('Checking .env file...');
25 |
26 | if (fs.existsSync(envPath)) {
27 | const env = getEnv(envPath);
28 | const exampleEnv = getEnv(exampleEnvPath);
29 | const envKeys = Object.keys(env);
30 | let missingKeys = Object.keys(exampleEnv).filter(key => !envKeys.includes(key));
31 | const keysToRenameForVite = envKeys.filter(key =>
32 | missingKeys.some(missingKey => missingKey === vitePrefix + key)
33 | );
34 |
35 | // for variables that client-side code needs to access, ensure their names begin with `VITE_`
36 | // https://vitejs.dev/guide/env-and-mode.html#env-files
37 | if (keysToRenameForVite.length > 0) {
38 | console.log('These keys in your .env file are not compatible with Vite:', keysToRenameForVite, '\n');
39 | console.log('Renaming incompatible keys...');
40 | let envText = fs.readFileSync(envPath, 'utf8');
41 | keysToRenameForVite.forEach(key => envText = envText.replace(key, vitePrefix + key));
42 | fs.writeFileSync(envPath, envText)
43 | console.log(green, `File updated: ${envPath}\n`);
44 | }
45 |
46 | missingKeys = missingKeys.filter(key =>
47 | !keysToRenameForVite
48 | .map(missingViteKey => vitePrefix + missingViteKey)
49 | .includes(key)
50 | );
51 |
52 | if (missingKeys.length > 0) {
53 | console.log('You are missing these keys in your .env file:', missingKeys, '\n');
54 | console.log('Copying missing keys to .env...');
55 | missingKeys.forEach(key => fs.appendFileSync(envPath, `\n${key}=${exampleEnv[key]}`));
56 | console.log(green, `File updated: ${envPath}\nDon't forget to update the values!`);
57 | } else {
58 | console.log(green, 'Your .env file has all required keys.');
59 | }
60 | } else {
61 | console.error(red, `No .env file found in ${__dirname}`);
62 | console.log('Creating .env file from .example.env...');
63 | fs.copyFileSync(exampleEnvPath, envPath);
64 | console.log(green, `File created: ${envPath}\nDon't forget to update the values!`);
65 | }
66 | }());
67 |
--------------------------------------------------------------------------------
/src/components/Loading/AcknowledgeModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | styled,
5 | Box,
6 | Modal,
7 | Typography,
8 | Link,
9 | Button,
10 | } from '@mui/material';
11 | import colors from '@theme/colors';
12 |
13 | const StyledModal = styled(Modal)({
14 | display: 'flex',
15 | alignItems: 'center',
16 | justifyContent: 'center',
17 | });
18 |
19 | const StyledBox = styled(Box)(({ theme }) => ({
20 | position: 'absolute',
21 | backgroundColor: '#29404F',
22 | padding: theme.spacing(4),
23 | boxShadow: theme.shadows[5],
24 | textAlign: 'center',
25 | maxWidth: '464px',
26 | borderRadius: '10px',
27 | outline: 'none',
28 | }));
29 |
30 | const ExternalLink = styled(Link)({
31 | color: colors.primaryFocus,
32 | textDecoration: 'none',
33 | '&:hover': {
34 | textDecoration: 'underline',
35 | },
36 | });
37 |
38 | const buttonStyle = {
39 | width: '104px',
40 | height: '29px',
41 | borderRadius: '5px',
42 | backgroundColor: '#ECECEC',
43 | border: '1px solid #ECECEC',
44 | '&:hover': {
45 | backgroundColor: '#DADADA',
46 | borderColor: '#DADADA',
47 | },
48 | color: '#29404F',
49 | fontWeight: '500',
50 | };
51 |
52 | function AcknowledgeModal({ onClose }) {
53 | const [open, setOpen] = React.useState(true);
54 | const handleClose = () => {
55 | setOpen(false);
56 | onClose();
57 | };
58 | return (
59 |
60 |
61 |
62 | Welcome to 311Data
63 |
64 |
69 | 311-data.org is 100% powered by volunteers from Hack
70 |
71 | for LA and is not affiliated with the city of Los Angeles.
72 |
73 | For official information about 311 services in Los
74 |
75 | Angeles, please visit
76 | {' '}
77 |
83 | MyLA311
84 |
85 | .
86 |
87 |
88 |
89 | Ok
90 |
91 |
92 |
93 |
94 | );
95 | }
96 |
97 | export default AcknowledgeModal;
98 |
99 | AcknowledgeModal.propTypes = {
100 | onClose: PropTypes.func.isRequired,
101 | };
102 |
--------------------------------------------------------------------------------
/src/scripts/csv_debug_tools/check_column_count.py:
--------------------------------------------------------------------------------
1 | """
2 | This script provides functions to check the number of columns in a CSV file.
3 | - `get_correct_column_count(file_path)`: Determines the number of columns based on the header row of the CSV.
4 | - `check_row_column_counts(file_path)`: Checks each row in the CSV to ensure it matches the column count of the header.
5 |
6 | Usage:
7 |
8 | Execute in terminal with python with script name, csv file to execute, flag to execute (header-count or row-check):
9 | "python3 script_name file_name flag-command"
10 | Note: make sure script and csv are in the same folder or define correct file path
11 |
12 | - To get the correct column count from the header, run in terminal:
13 | example command: `python3 check_column_count.py 2021.csv header-count`
14 |
15 | - To check if each row has the correct number of columns:
16 | example command: `python3 check_column_count.py 2021.csv row-check`
17 | """
18 |
19 | import csv
20 | import sys
21 |
22 | def get_correct_column_count(file_path):
23 | """
24 | Determine the number of columns based on the header row of the CSV.
25 | """
26 | with open(file_path, "r") as file:
27 | header = file.readline().strip()
28 | return len(header.split(','))
29 |
30 |
31 | def check_row_column_counts(file_path):
32 | """
33 | Check each row in the CSV to ensure it matches the column count of the header.
34 | """
35 | correct_columns = get_correct_column_count(file_path)
36 | incorrect_rows = []
37 |
38 | with open(file_path, "r") as file:
39 | reader = csv.reader(file)
40 | header = next(reader) # Skip the header row
41 | for line_number, row in enumerate(reader, start=2): # Start at 2 to account for the header row
42 | if len(row) != correct_columns:
43 | incorrect_rows.append((line_number, len(row)))
44 |
45 | if incorrect_rows:
46 | print(f"Found rows with incorrect number of columns:")
47 | for line_number, column_count in incorrect_rows:
48 | print(f"Line {line_number} has {column_count} columns instead of {correct_columns}")
49 | else:
50 | print("All rows have the correct number of columns.")
51 |
52 |
53 | if __name__ == "__main__":
54 |
55 | # Provide instructions if not enough args passed to the script
56 | if len(sys.argv) < 3:
57 | print("Usage: python script_name.py ")
58 | print("Commands: header-count, row-check")
59 | sys.exit(1)
60 |
61 | file_path = sys.argv[1]
62 | command = sys.argv[2]
63 |
64 | # Flags definition
65 | if command == "header-count":
66 | correct_columns = get_correct_column_count(file_path)
67 | print(f"Correct number of columns: {correct_columns}")
68 | elif command == "row-check":
69 | check_row_column_counts(file_path)
70 | else:
71 | print("Unknown command. Use 'header-count' or 'row-check'.")
72 |
--------------------------------------------------------------------------------
/src/components/contact/googleFormScript.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * This script is run in Google Apps Script for handling form submissions from the contact
4 | * page Google Form.
5 | * It is not run in this codebase but we are storing a copy here for documentation.
6 | *
7 | * This script automates the process of creating GitHub issues upon form submission.
8 | * It includes the following key functions:
9 | *
10 | * Functions:
11 | * - onFormSubmit: Triggered when the form is submitted. Extracts form
12 | * responses and creates a GitHub issue.
13 | * - setUpTrigger: Sets up the form submit trigger for the Google Form.
14 | *
15 | * Usage:
16 | * - Set up the trigger by running the setUpTrigger function.
17 | * - This script should be copied and stored in the web app codebase for
18 | * reference and documentation purposes.
19 | *
20 | * GitHub Issue Creation:
21 | * - Constructs the title and body of the issue using form responses.
22 | * - Sends a POST request to the GitHub API to create the issue.
23 | *
24 | * Apps Script documenation on Form Service:
25 | * - https://developers.google.com/apps-script/reference/forms
26 | */
27 |
28 | function onFormSubmit(e) {
29 | const formResponse = e.response; // Use the FormResponse object
30 | const itemResponses = formResponse.getItemResponses(); // Get all item responses
31 |
32 | // Extract responses from the form questions
33 | const fullName = itemResponses[0].getResponse(); // Full Name
34 | const email = itemResponses[1].getResponse(); // Email
35 | const neighborhoodAssociation = itemResponses[2].getResponse() || 'Not provided'; // Neighborhood Association
36 | const message = itemResponses[3].getResponse(); // Message
37 |
38 | // Construct title and body for GitHub issue
39 | const title = `Feedback from ${fullName} (${email})`;
40 | const body = `**Full Name:** ${fullName}\n**Email:** ${email}\n**Neighborhood Association:** ${neighborhoodAssociation}\n**Message:**\n${message}`;
41 |
42 | // GitHub API configuration
43 | const GITHUB_ORG = 'hackforla';
44 | const GITHUB_REPO = '311-data';
45 | const GITHUB_TOKEN = PropertiesService.getScriptProperties().getProperty('GITHUB_TOKEN');
46 |
47 | const url = `https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/issues`;
48 | const payload = {
49 | 'title': title,
50 | 'body': body
51 | };
52 |
53 | const options = {
54 | 'method': 'post',
55 | 'contentType': 'application/json',
56 | 'headers': {
57 | 'Authorization': `token ${GITHUB_TOKEN}`
58 | },
59 | 'payload': JSON.stringify(payload)
60 | };
61 |
62 | // Sending the request to create the GitHub issue
63 | const response = UrlFetchApp.fetch(url, options);
64 | Logger.log(response.getContentText());
65 | }
66 |
67 | function setUpTrigger() {
68 | const form = FormApp.getActiveForm();
69 | ScriptApp.newTrigger('onFormSubmit')
70 | .forForm(form)
71 | .onFormSubmit()
72 | .create();
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/src/components/Loading/LoadingModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { keyframes } from '@mui/system';
3 | import { styled } from '@mui/material/styles';
4 | import {
5 | Box, Modal, Typography, Link,
6 | } from '@mui/material';
7 | import fonts from '@theme/fonts';
8 | import LoadingModal311Logo from '@assets/311Logo.png';
9 | import HFLALogo from '@assets/hack_for_la_logo.png';
10 | import spinner from '@assets/spinner.png';
11 | import colors from '@theme/colors';
12 |
13 | const StyledModal = styled(Modal)({
14 | display: 'flex',
15 | alignItems: 'center',
16 | justifyContent: 'center',
17 | });
18 |
19 | const StyledBox = styled(Box)(({ theme }) => ({
20 | position: 'absolute',
21 | bottom: '35vh',
22 | backgroundColor: '#29404F',
23 | padding: theme.spacing(4),
24 | boxShadow: theme.shadows[5],
25 | textAlign: 'center',
26 | maxWidth: '533px',
27 | maxHeight: '469px',
28 | borderRadius: '20px',
29 | outline: 'none',
30 | }));
31 |
32 | const StyledTypography = styled(Typography)({
33 | fontSize: '16px',
34 | fontFamily: fonts.family.roboto,
35 | fontWeight: fonts.weight.medium,
36 | });
37 |
38 | const ExternalLink = styled(Link)({
39 | color: colors.primaryFocus,
40 | textDecoration: 'none',
41 | '&:hover': {
42 | textDecoration: 'underline',
43 | },
44 | });
45 |
46 | // Loading png spinner animation
47 | const spin = keyframes`
48 | from { transform: rotate(0deg); }
49 | to { transform: rotate(360deg); }
50 | `;
51 |
52 | const StyledSpinner = styled('img')({
53 | animation: `${spin} 2s linear infinite`,
54 | width: '36px',
55 | display: 'block',
56 | margin: '10px auto',
57 | });
58 |
59 | export default function LoadingModal() {
60 | return (
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | Loading data points and map. Please give us a moment.
69 |
70 | For official information about 311 services in Los Angeles,
71 |
72 | please visit
73 | {' '}
74 |
75 | MyLA311
76 |
77 | .
78 |
79 |
83 |
84 | Powered by Volunteers at Hack for LA
85 |
86 |
87 |
88 |
89 |
90 | );
91 | }
92 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/blank-research-plan.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Blank Research Plan
3 | about: Template for UX researcher to create research plan.
4 | title: ''
5 | labels: 'Complexity: Missing, Feature: Research, Milestone: Missing, role: UI/UX Research,
6 | size: Missing'
7 | assignees: ''
8 |
9 | ---
10 |
11 | ### Dependencies
12 | - [ ] [Replace this with Roadmap issue #]
13 | - [ ] A research setup structure has been created and approved.
14 |
15 | ### Overview
16 | We need to create a Research Plan for [NAME OF RESEARCH PLAN] so that [RESEARCH GOAL(S)].
17 |
18 | ### Action Items
19 | - [ ] Update this issue to make sure it's properly categorized and easy to manage
20 | - [ ] Under "Projects," add to the Project Management Board (helps with Project Management overview)
21 | - [ ] Add the milestone: [07 - Research Plan Creation V1.2](https://github.com/hackforla/311-data/milestone/35) (helps with prioritization)
22 | - [ ] Add label of the correct research plan number associated with this research plan (e.g., Research: RPV1.2 - Moderated Test) (helps you find related issues in the same workflow)
23 | - [ ] Edit title of issue with the correct research plan number (same as prior)
24 | - [ ] Update this issue with the relevant Resources:
25 | - [ ] Go to the Google Drive's Research by Type Folder (link in Resources)
26 | - [ ] Go into the relevant Type Folder
27 | - [ ] once inside, right-click on the relevant destination folder and copy the link.
28 | - [ ] Add the Google Drive link in the Resources below with the link you just copied and revise the display text to include the Research Plan name
29 | - [ ] Create the Research Plan Document.
30 | - [ ] Make a new document using the 311 Data Research Plan template (link in Resources). Follow the instructions in the How To Write a Research Plan guide (link in Resources) and mark your progress in this issue.
31 | - [ ] Move your document from "My Drive" to the shared drive 311 Data folder of this specific RP.
32 | - [ ] Name your document to be called "311 Data: RP___ : [TITLE OF RESEARCH] Research Plan".
33 | - [ ] Update the document with the relevant information in accordance with the research roadmap document
34 | - [ ] Update the Table of contents (page 2) when you are finished with the document.
35 | - [ ] Once research lead has signed off, add it to the PM/Research lead agenda
36 | - [ ] Sign-off by PM
37 | - [ ] Does this issue have any dependency checkboxes in the action items here?
38 | - [ ] If yes, remove dependencies that are listed below (issues that are waiting on this issue to be completed. Sometimes they have already been made and sometimes they yet to be made).
39 | - [ ] Close this issue
40 | - [ ] If no, check to see if the follow-up issues have already been made. If new issues have not been made, then apply label `ready for research lead` and define what needs to be done in a comment below.
41 | - [ ] Close the issue
42 | - [ ] Move to `Question/Review` Column
43 |
44 |
45 | ### Resources
46 | - Google Drive RP__ [Folder](https://drive.google.com/drive/u/0/folders/19oXFkecEclzt4HQvL3tMOY4L0N8pqpRa)
47 | - [TWE: How to Write a Research Plan](https://docs.google.com/document/d/1Cwc0w4ZPUI8989w3jU8BW2LzLK_Tl5gHdI0VxN5ej0o/edit)
48 |
--------------------------------------------------------------------------------
/src/scripts/updateHfDataset.py:
--------------------------------------------------------------------------------
1 | import duckdb
2 | import requests
3 | import os
4 | import glob
5 | from tqdm import tqdm
6 | from huggingface_hub import HfApi, login
7 | from dotenv import load_dotenv
8 | load_dotenv()
9 |
10 | # set environment as 'dev' or 'prod'
11 | ENV = os.getenv('VITE_ENV')
12 |
13 | if ENV == 'DEV':
14 | HF_USERNAME = '311-Data-Dev'
15 | elif ENV == 'PROD':
16 | HF_USERNAME = '311-data'
17 | else:
18 | # exit out of the program with an error message
19 | print('Incorrect environment variable set for VITE_ENV.')
20 | exit(1)
21 |
22 | def dlData():
23 | '''
24 | Download the current year's dataset from data.lacity.org
25 | '''
26 | url = "https://data.lacity.org/api/views/h73f-gn57/rows.csv?accessType=DOWNLOAD"
27 | outfile = "2025.csv"
28 |
29 | response = requests.get(url, stream=True)
30 |
31 | # Save downloaded file
32 | with open(outfile, "wb") as file:
33 | for data in tqdm(response.iter_content()):
34 | file.write(data)
35 |
36 |
37 | def hfClean():
38 | '''
39 | Clean the dataset by removing problematic string combinations and update timestamp to ISO format
40 | '''
41 | infile = "2025.csv"
42 | fixed_filename = "2025-fixed.csv"
43 | clean_filename = "2025-clean.parquet"
44 |
45 | # List of problmenatic strings to be replaced with ""
46 | replace_strings = ["VE, 0"]
47 |
48 | conn = duckdb.connect(database=':memory:')
49 |
50 | try:
51 | # Clean and save modified file
52 | with open(infile, "r") as input_file, open(fixed_filename, "w") as output_file:
53 | for line in input_file:
54 | for replace_string in replace_strings:
55 | line = line.replace(replace_string, "")
56 | output_file.write(line)
57 |
58 | # Open modified file and perform an import/export to duckdb to ensure timestamps are formatted correctly
59 | conn.execute(
60 | f"create table requests as select * from read_csv_auto('{fixed_filename}', header=True, timestampformat='%m/%d/%Y %H:%M:%S %p');")
61 | conn.execute(
62 | f"copy (select * from requests) to '{clean_filename}' with (FORMAT PARQUET);")
63 |
64 | except FileNotFoundError:
65 | print(f"File {infile} not found.")
66 |
67 |
68 | def hfUpload():
69 | '''
70 | Upload the clean dataset to huggingface.co
71 | '''
72 | local_filename = '2025-clean.parquet'
73 | dest_filename = '2025.parquet'
74 | repo_name = '2025'
75 | repo_type = 'dataset'
76 |
77 | repo_id = f"{HF_USERNAME}/{repo_name}"
78 | TOKEN = os.getenv('HUGGINGFACE_LOGIN_TOKEN')
79 |
80 | login(TOKEN)
81 | api = HfApi()
82 | api.upload_file(
83 | path_or_fileobj=local_filename,
84 | path_in_repo=dest_filename,
85 | repo_id=repo_id,
86 | repo_type=repo_type,
87 | )
88 |
89 |
90 | def cleanUp():
91 | for file in glob.glob('*.csv'):
92 | os.remove(file)
93 | for file in glob.glob('*.parquet'):
94 | os.remove(file)
95 |
96 |
97 | def main():
98 | dlData()
99 | hfClean()
100 | hfUpload()
101 | cleanUp()
102 |
103 |
104 | main()
105 |
--------------------------------------------------------------------------------
/src/redux/reducers/metadata.js:
--------------------------------------------------------------------------------
1 | import tempTypes from '@data/requestTypes';
2 |
3 | export const types = {
4 | GET_METADATA_REQUEST: 'GET_METADATA_REQUEST',
5 | GET_METADATA_SUCCESS: 'GET_METADATA_SUCCESS',
6 | GET_METADATA_FAILURE: 'GET_METADATA_FAILURE',
7 | GET_REQUEST_TYPES_SUCCESS: 'GET_REQUEST_TYPES_SUCCESS',
8 | GET_COUNCILS_SUCCESS: 'GET_COUNCILS_SUCCESS',
9 | GET_REGIONS_SUCCESS: 'GET_REGIONS_SUCCESS',
10 | GET_AGENCIES_SUCCESS: 'GET_AGENCIES_SUCCESS',
11 | GET_NC_GEOJSON_SUCCESS: 'GET_NC_GEOJSON_SUCCESS',
12 | };
13 |
14 | export const getMetadataRequest = () => ({
15 | type: types.GET_METADATA_REQUEST,
16 | });
17 |
18 | export const getMetadataSuccess = response => ({
19 | type: types.GET_METADATA_SUCCESS,
20 | payload: response,
21 | });
22 |
23 | export const getMetadataFailure = error => ({
24 | type: types.GET_METADATA_FAILURE,
25 | payload: error,
26 | });
27 |
28 | export const getRequestTypesSuccess = response => ({
29 | type: types.GET_REQUEST_TYPES_SUCCESS,
30 | payload: response,
31 | });
32 |
33 | export const getCouncilsSuccess = response => ({
34 | type: types.GET_COUNCILS_SUCCESS,
35 | payload: response,
36 | });
37 |
38 | export const getRegionsSuccess = response => ({
39 | type: types.GET_REGIONS_SUCCESS,
40 | payload: response,
41 | });
42 |
43 | export const getAgenciesSuccess = response => ({
44 | type: types.GET_AGENCIES_SUCCESS,
45 | payload: response,
46 | });
47 |
48 | export const getNcGeojsonSuccess = response => ({
49 | type: types.GET_NC_GEOJSON_SUCCESS,
50 | payload: response,
51 | });
52 |
53 | const initialState = {
54 | currentTimeUTC: null,
55 | currentTimeLocal: null,
56 | gitSha: null,
57 | version: null,
58 | lastPulledUTC: null,
59 | lastPulledLocal: null,
60 | requestTypes: tempTypes,
61 | councils: null,
62 | regions: null,
63 | agencies: null,
64 | ncGeojson: null,
65 | };
66 |
67 | export default (state = initialState, action) => {
68 | switch (action.type) {
69 | case types.GET_METADATA_SUCCESS:
70 | return {
71 | ...state,
72 | ...action.payload,
73 | };
74 | case types.GET_REQUEST_TYPES_SUCCESS:
75 | return {
76 | ...state,
77 | requestTypes: action.payload,
78 | };
79 | case types.GET_COUNCILS_SUCCESS:
80 | return {
81 | ...state,
82 | councils: action.payload,
83 | };
84 | case types.GET_REGIONS_SUCCESS:
85 | return {
86 | ...state,
87 | regions: action.payload,
88 | };
89 | case types.GET_AGENCIES_SUCCESS:
90 | return {
91 | ...state,
92 | agencies: action.payload,
93 | };
94 | case types.GET_NC_GEOJSON_SUCCESS:
95 | return {
96 | ...state,
97 | ncGeojson: action.payload,
98 | };
99 | case types.GET_METADATA_FAILURE: {
100 | const {
101 | response: { status },
102 | message,
103 | } = action.payload;
104 |
105 | return {
106 | ...state,
107 | error: {
108 | code: status,
109 | message,
110 | error: action.payload,
111 | },
112 | };
113 | }
114 | default:
115 | return state;
116 | }
117 | };
118 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
55 |
56 |
57 |
58 |
59 | You need to enable JavaScript to run this app.
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/assets/datepicker.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/redux/tempTypesApi.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | /* tempTypes original file */
4 |
5 | import colors from '@theme/colors';
6 |
7 | const tempTypes = [
8 | {
9 | typeId: 1,
10 | typeName: 'Graffiti',
11 | agencyId: 4,
12 | agencyName: 'Office of Community Beautification',
13 | color: '#BF82BA',
14 | description: 'Graffiti on walls/bulidings, unpainted concrete surfaces or metal posts',
15 | },
16 | {
17 | typeId: 2,
18 | typeName: 'Homeless Encampment',
19 | agencyId: 2,
20 | agencyName: 'Sanitation Bureau',
21 | color: '#11975F',
22 | description: 'Encampments impacting right-of-way or maintenance of clean and sanitary public areas',
23 | },
24 | {
25 | typeId: 3,
26 | typeName: 'Animal Remains',
27 | agencyId: 2,
28 | agencyName: 'Sanitation Bureau',
29 | color: '#267370',
30 | description: 'Dead animal located on the streets or outside of residences',
31 | },
32 | {
33 | typeId: 4,
34 | typeName: 'Bulky Items',
35 | agencyId: 2,
36 | agencyName: 'Sanitation Bureau',
37 | color: '#D05F4E',
38 | description: 'Chairs, desks, mattress and more...',
39 | },
40 | {
41 | typeId: 5,
42 | typeName: 'Electronic Waste',
43 | agencyId: 2,
44 | agencyName: 'Sanitation Bureau',
45 | color: '#AE3D51',
46 | description: 'Computers, microwaves, laptops and more...',
47 | },
48 | {
49 | typeId: 6,
50 | typeName: 'Illegal Dumping',
51 | agencyId: 2,
52 | agencyName: 'Sanitation Bureau',
53 | color: '#685DB1',
54 | description: 'Disposing of garbage, waste and other matter on public or private property',
55 | },
56 | {
57 | typeId: 7,
58 | typeName: 'Metal/Appliances',
59 | agencyId: 2,
60 | agencyName: 'Sanitation Bureau',
61 | color: '#8B508B',
62 | description: 'Air conditioners, dryers, refrigerator and more...',
63 | },
64 | {
65 | typeId: 8,
66 | typeName: 'Single Streetlight',
67 | agencyId: 1,
68 | agencyName: 'Street Lighting Bureau',
69 | color: '#79B74E',
70 | description: 'Pole knocked down, streetlight outage on a wooden power pole, or malfunctioning traffic signal',
71 | },
72 | {
73 | typeId: 9,
74 | typeName: 'Multiple Streetlights',
75 | agencyId: 1,
76 | agencyName: 'Street Lighting Bureau',
77 | color: '#EDAD08',
78 | description: 'Multiple poles knocked down, streetlight outages on wooden power poles, or malfunctioning traffic signals',
79 | },
80 | {
81 | typeId: 10,
82 | typeName: 'Water Waste',
83 | agencyId: 1,
84 | agencyName: 'Street Lighting Bureau',
85 | color: '#E17C05',
86 | description: 'Water runoff, over-watering, incorrect water days, or any other water waste ',
87 | },
88 | {
89 | typeId: 11,
90 | typeName: 'Other',
91 | agencyId: 0,
92 | agencyName: null,
93 | color: '#333333',
94 | description: 'Issues that do not fit into any of the other available types',
95 | },
96 | {
97 | typeId: 12,
98 | typeName: 'Feedback',
99 | agencyId: 0,
100 | agencyName: null,
101 | color: '#666666',
102 | description: "Either follow up on other issues or something that doesn't fit into the other types",
103 | },
104 | ];
105 |
106 | export default tempTypes;
107 |
--------------------------------------------------------------------------------
/src/components/layout/Main/CookieNotice.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import makeStyles from '@mui/styles/makeStyles';
4 | import { Link } from 'react-router-dom';
5 | import { connect } from 'react-redux';
6 | import Card from '@mui/material/Card';
7 | import CardActions from '@mui/material/CardActions';
8 | import CardContent from '@mui/material/CardContent';
9 | import { CardHeader } from '@mui/material';
10 | import Button from '@mui/material/Button';
11 | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
12 | import { acceptCookies } from '@reducers/ui';
13 | import colors from '@theme/colors';
14 |
15 | const useStyles = makeStyles(theme => ({
16 | root: {
17 | width: 325,
18 | backgroundColor: colors.primaryLight,
19 | zIndex: 50000,
20 | position: 'absolute',
21 | bottom: 0,
22 | right: 0,
23 | },
24 | title: {
25 | ...theme.typography.body1,
26 | fontSize: '17px',
27 | fontWeight: 700,
28 | paddingTop: 16,
29 | paddingLeft: 16,
30 | },
31 | headStyle: {
32 | padding: 8,
33 | backgroundColor: colors.secondaryFocus,
34 | },
35 | iconStyle: {
36 | verticalAlign: 'text-top',
37 | },
38 | copyStyle: {
39 | paddingTop: 5,
40 | fontSize: 12,
41 | '&:last-child': {
42 | paddingBottom: 5,
43 | },
44 | },
45 | linkStyle: {
46 | padding: 0,
47 | color: colors.primaryFocus,
48 | marginLeft: 3,
49 | textDecoration: 'none',
50 | },
51 | }));
52 |
53 | function CookieNotice({
54 | showCookieNotice,
55 | acceptCookieNotice,
56 | }) {
57 | const classes = useStyles();
58 | const handleClick = () => {
59 | acceptCookieNotice();
60 | sessionStorage.setItem('accept-cookies', true);
61 | };
62 |
63 | if (showCookieNotice) {
64 | return (
65 |
66 |
67 |
68 |
69 | {' '}
70 | Cookies and Privacy Policy
71 |
72 |
73 |
74 | We use cookies and other tracking technologies to improve your browsing
75 | experience and to better understand our website traffic. By browsing our
76 | website, you consent to our use of cookies and other tracking technologies.
77 | Learn more
78 |
79 | Got it!
80 |
81 |
82 |
83 |
84 | );
85 | }
86 | return null;
87 | }
88 |
89 | const mapStateToProps = state => ({
90 | showCookieNotice: !state.ui.cookiesAccepted,
91 | });
92 |
93 | const mapDispatchToProps = dispatch => ({
94 | acceptCookieNotice: () => dispatch(acceptCookies()),
95 | });
96 |
97 | CookieNotice.propTypes = {
98 | showCookieNotice: PropTypes.bool.isRequired,
99 | acceptCookieNotice: PropTypes.func.isRequired,
100 | };
101 |
102 | export default connect(mapStateToProps, mapDispatchToProps)(CookieNotice);
103 |
--------------------------------------------------------------------------------
/src/assets/aboutmobile.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/components/layout/Footer/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { connect } from 'react-redux';
3 | import Typography from '@mui/material/Typography';
4 | import makeStyles from '@mui/styles/makeStyles';
5 | import { Link } from 'react-router-dom';
6 | import LastUpdated from '@components/layout/Footer/LastUpdated';
7 | import SocialMediaLinks from '@components/layout/Footer/SocialMediaLinks';
8 | import { toNonBreakingSpaces } from '@utils';
9 | import HFLALogo from '@assets/hack_for_la_logo.png';
10 |
11 | // Footer should make use of style overrides to look the same regardless of light/dark theme.
12 | const useStyles = makeStyles(theme => ({
13 | footer: {
14 | position: 'fixed',
15 | bottom: 0,
16 | height: theme.footer.height,
17 | width: '100%',
18 | backgroundColor: theme.palette.primary.dark,
19 | zIndex: 1400,
20 | },
21 | footerSpacing: {
22 | height: theme.footer.height,
23 | },
24 | copyright: {
25 | display: 'flex',
26 | gap: theme.spacing(1),
27 | fontWeight: theme.typography.fontWeightMedium,
28 | lineHeight: theme.footer.height,
29 | color: theme.palette.text.dark,
30 | },
31 | container: {
32 | display: 'flex',
33 | flexDirection: 'row',
34 | justifyContent: 'space-between',
35 | margin: theme.spacing(0, 2, 0),
36 | },
37 | copyrightContainer: {
38 | display: 'flex',
39 | alignItems: 'center',
40 | },
41 | link: {
42 | color: theme.palette.text.dark,
43 | textDecoration: 'none',
44 | },
45 | logo: {
46 | marginInline: theme.spacing(1),
47 | },
48 | }));
49 |
50 | function Footer() {
51 | const classes = useStyles();
52 | const currentDate = new Date();
53 | const footerItems = [
54 | { text: `\u00a9 ${currentDate.getFullYear()} 311 Data` },
55 | { text: 'All Rights Reserved' },
56 | { text: 'Privacy Policy', href: '/privacy' },
57 | { text: 'Powered by volunteers from Hack for LA' },
58 | ];
59 |
60 | return (
61 |
62 |
63 |
64 |
65 | {footerItems.map(({ text, href }, i) => {
66 | const nonBreakingText = toNonBreakingSpaces(text);
67 |
68 | return (
69 |
70 | {href ? (
71 |
72 | {nonBreakingText}
73 |
74 | ) : (
75 | {nonBreakingText}
76 | )}
77 |
78 | {i === footerItems.length - 1 ? null : | }
79 |
80 | );
81 | })}
82 |
83 |
84 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | );
99 | }
100 |
101 | const mapStateToProps = state => ({
102 | lastUpdated: state.metadata.lastPulledLocal,
103 | });
104 |
105 | export default connect(mapStateToProps, null)(Footer);
106 |
--------------------------------------------------------------------------------