diff --git a/src/components/main/header/Header.jsx b/src/components/main/header/Header.jsx
index 639131093..d709ab035 100644
--- a/src/components/main/header/Header.jsx
+++ b/src/components/main/header/Header.jsx
@@ -26,6 +26,8 @@ const Header = () => {
background: COLORS.BRAND.MAIN,
height: '60px',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
+ // Really high z-index here to ensure Header is on top of modal
+ zIndex: '20000',
}}
>
diff --git a/src/components/main/menu/DateSelector/CustomDateInput.jsx b/src/components/main/menu/DateSelector/CustomDateInput.jsx
new file mode 100644
index 000000000..3cd7d4150
--- /dev/null
+++ b/src/components/main/menu/DateSelector/CustomDateInput.jsx
@@ -0,0 +1,76 @@
+import React, { forwardRef } from 'react';
+import PropTypes from 'proptypes';
+import classNames from 'classnames';
+
+// NOTE: This component is not currently 100% working and is not in use.
+// To be completed after MVP.
+
+const CustomDateInput = ({
+ value,
+ onClick,
+ onChange,
+ color,
+ size,
+ rounded,
+ hovered,
+ focused,
+ loading,
+ disabled,
+ readOnly,
+ isStatic,
+}, ref) => {
+ const inputClassName = classNames('input', {
+ [`is-${color}`]: color,
+ [`is-${size}`]: size,
+ 'is-rounded': rounded,
+ 'is-hovered': hovered,
+ 'is-focused': focused,
+ 'is-loading': loading,
+ 'is-static': isStatic,
+ });
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default forwardRef(CustomDateInput);
+
+CustomDateInput.propTypes = {
+ value: PropTypes.string,
+ onClick: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ color: PropTypes.oneOf(['primary', 'info', 'success', 'warning', 'danger']),
+ size: PropTypes.oneOf([undefined, 'small', 'medium', 'large']),
+ rounded: PropTypes.bool,
+ hovered: PropTypes.bool,
+ focused: PropTypes.bool,
+ loading: PropTypes.bool,
+ disabled: PropTypes.bool,
+ readOnly: PropTypes.bool,
+ isStatic: PropTypes.bool,
+};
+
+CustomDateInput.defaultProps = {
+ value: '',
+ color: 'primary',
+ size: undefined,
+ rounded: false,
+ hovered: false,
+ focused: false,
+ loading: false,
+ disabled: false,
+ readOnly: false,
+ isStatic: false,
+};
diff --git a/src/components/main/menu/DateSelector/DateRangePicker.jsx b/src/components/main/menu/DateSelector/DateRangePicker.jsx
new file mode 100644
index 000000000..97f6da8a9
--- /dev/null
+++ b/src/components/main/menu/DateSelector/DateRangePicker.jsx
@@ -0,0 +1,219 @@
+import React, { useState } from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'proptypes';
+import moment from 'moment';
+import DatePicker from 'react-datepicker';
+
+import {
+ updateStartDate,
+ updateEndDate,
+} from '../../../../redux/reducers/data';
+
+import Button from '../../../common/Button';
+import Icon from '../../../common/Icon';
+
+import 'react-datepicker/dist/react-datepicker.css';
+import COLORS from '../../../../styles/COLORS';
+
+const cardStyle = {
+ height: '299px',
+ width: '400px',
+ overflow: 'visible',
+ boxShadow: '0px 6px 5px rgba(0, 0, 0, 0.5)',
+};
+
+const headerStyle = {
+ height: '50px',
+ background: COLORS.BACKGROUND,
+ color: COLORS.FONTS,
+ fontWeight: 'bold',
+ fontSize: '20px',
+ border: 'none',
+ borderRadius: '0',
+};
+
+const textStyle = {
+ fontSize: '16px',
+};
+
+const inputSpanStyle = {
+ display: 'inline-block',
+ width: '100px',
+ color: headerStyle.color,
+ ...textStyle,
+};
+
+const containerDivStyle = {
+ height: '42px',
+ width: '317px',
+ display: 'flex',
+ alignItems: 'center',
+};
+
+const DateRangePicker = ({
+ id,
+ title,
+ style,
+ handleClick,
+ updateStart,
+ updateEnd,
+}) => {
+ const [startDate, updateLocalStart] = useState();
+ const [endDate, updateLocalEnd] = useState();
+
+ const handleDateChange = (updateStartOrEndDate, date) => {
+ updateStartOrEndDate(date);
+ };
+
+ return (
+
+ {/* ---------- Modal Card Header ---------- */}
+
+
+ {/* ---------- Modal Card Body - main content ---------- */}
+
+
+
+
+
+ Start Date
+
+ handleDateChange(updateLocalStart, date)}
+ placeholderText="MM/DD/YYYY"
+ />
+
+
+
+
+ End Date
+
+ handleDateChange(updateLocalEnd, date)}
+ placeholderText="MM/DD/YYYY"
+ />
+
+
+
+
+ {/* ---------- Modal Card Footer - button(s) ---------- */}
+
+
+ );
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ updateStart: (newStartDate) => dispatch(updateStartDate(newStartDate)),
+ updateEnd: (newEndDate) => dispatch(updateEndDate(newEndDate)),
+});
+
+export default connect(
+ null,
+ mapDispatchToProps,
+)(DateRangePicker);
+
+DateRangePicker.propTypes = {
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string,
+ style: PropTypes.shape({}),
+ handleClick: PropTypes.func.isRequired,
+ updateStart: PropTypes.func.isRequired,
+ updateEnd: PropTypes.func.isRequired,
+};
+
+DateRangePicker.defaultProps = {
+ title: undefined,
+ style: undefined,
+};
diff --git a/src/components/main/menu/DateSelector/DateSelector.jsx b/src/components/main/menu/DateSelector/DateSelector.jsx
new file mode 100644
index 000000000..9b7524f39
--- /dev/null
+++ b/src/components/main/menu/DateSelector/DateSelector.jsx
@@ -0,0 +1,159 @@
+import React, { useState, useEffect } from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'proptypes';
+import moment from 'moment';
+import {
+ updateStartDate,
+ updateEndDate,
+} from '../../../../redux/reducers/data';
+
+import Dropdown from '../../../common/Dropdown';
+import Modal from '../../../common/Modal';
+import DateRangePicker from './DateRangePicker';
+
+import COLORS from '../../../../styles/COLORS';
+
+const getDates = (dateOptionValue) => {
+ let newStartDate;
+ const newEndDate = moment().format('MM/DD/YYYY');
+ const formatPriorDate = (num, timeInterval) => moment().subtract(num, timeInterval).format('MM/DD/YYYY');
+
+ switch (dateOptionValue) {
+ case 'LAST_WEEK':
+ newStartDate = formatPriorDate(1, 'week');
+ break;
+ case 'LAST_MONTH':
+ newStartDate = formatPriorDate(1, 'month');
+ break;
+ case 'LAST_6_MONTHS':
+ newStartDate = formatPriorDate(6, 'months');
+ break;
+ case 'LAST_12_MONTHS':
+ newStartDate = formatPriorDate(12, 'months');
+ break;
+ case 'YEAR_TO_DATE':
+ newStartDate = moment().startOf('year').format('MM/DD/YYYY');
+ break;
+
+ // comment below circumvents eslint(default-case)
+ // no default
+ }
+ return { newStartDate, newEndDate };
+};
+
+const DateSelector = ({
+ style,
+ startDate,
+ endDate,
+ updateStart,
+ updateEnd,
+}) => {
+ const placeHolder = 'MM/DD/YYYY';
+ const dateRangeOptions = [
+ { label: 'Last Week', value: 'LAST_WEEK' },
+ { label: 'Last Month', value: 'LAST_MONTH' },
+ { label: 'Last 6 Months', value: 'LAST_6_MONTHS' },
+ { label: 'Last 12 months', value: 'LAST_12_MONTHS' },
+ { label: 'Year to Date', value: 'YEAR_TO_DATE' },
+ { label: 'Custom Date Range', value: 'CUSTOM_DATE_RANGE' },
+ ];
+ const [modalOpen, setModalOpen] = useState(false);
+
+ useEffect(() => {
+ const handleEscapeClick = (e) => {
+ if (e.keyCode !== 27) {
+ return;
+ }
+ setModalOpen(false);
+ };
+
+ if (modalOpen) {
+ document.addEventListener('keydown', handleEscapeClick);
+ } else {
+ document.removeEventListener('keydown', handleEscapeClick);
+ }
+
+ return () => {
+ document.removeEventListener('keydown', handleEscapeClick);
+ };
+ }, [modalOpen]);
+
+ return (
+
+
+
+ Date Range Selection
+
+
+
+
+ {`Start ${startDate || placeHolder} To ${endDate || placeHolder}`}
+
+
+
+ {
+ if (dateOption !== 'CUSTOM_DATE_RANGE') {
+ const { newStartDate, newEndDate } = getDates(dateOption);
+ updateStart(newStartDate);
+ updateEnd(newEndDate);
+ } else {
+ setModalOpen(true);
+ }
+ }}
+ />
+
+
setModalOpen(false)}
+ />
+ )}
+ />
+
+ );
+};
+
+const mapStateToProps = (state) => ({
+ startDate: state.data.startDate,
+ endDate: state.data.endDate,
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ updateStart: (newStartDate) => dispatch(updateStartDate(newStartDate)),
+ updateEnd: (newEndDate) => dispatch(updateEndDate(newEndDate)),
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(DateSelector);
+
+DateSelector.propTypes = {
+ updateStart: PropTypes.func.isRequired,
+ updateEnd: PropTypes.func.isRequired,
+ style: PropTypes.shape({}),
+ startDate: PropTypes.string,
+ endDate: PropTypes.string,
+};
+
+DateSelector.defaultProps = {
+ style: undefined,
+ startDate: undefined,
+ endDate: undefined,
+};
diff --git a/src/components/main/menu/Menu.jsx b/src/components/main/menu/Menu.jsx
index 923c5b0fc..49c752134 100644
--- a/src/components/main/menu/Menu.jsx
+++ b/src/components/main/menu/Menu.jsx
@@ -1,6 +1,13 @@
+/* eslint-disable jsx-a11y/anchor-is-valid */
+/* eslint-disable jsx-a11y/no-static-element-interactions */
+/* eslint-disable jsx-a11y/click-events-have-key-events */
import React, { useState } from 'react';
import { slide as Sidebar } from 'react-burger-menu';
+
import Button from '../../common/Button';
+import DateSelector from './DateSelector/DateSelector';
+import NCSelector from './NCSelector';
+import RequestTypeSelector from './RequestTypeSelector';
// const buildDataUrl = () => {
// return `https://data.lacity.org/resource/${dataResources[year]}.json?$select=location,zipcode,address,requesttype,status,ncname,streetname,housenumber&$where=date_extract_m(CreatedDate)+between+${startMonth}+and+${endMonth}+and+requesttype='${request}'`;
@@ -24,11 +31,10 @@ const Menu = () => {
return (
{
styles={{
bmMenu: {
background: 'white',
- boxShadow: '0 4px 5px grey',
+ boxShadow: '0px 4px 5px rgba(108, 108, 108, 0.3)',
},
}}
>
-
diff --git a/src/components/main/menu/NCSelector.jsx b/src/components/main/menu/NCSelector.jsx
new file mode 100644
index 000000000..37639235d
--- /dev/null
+++ b/src/components/main/menu/NCSelector.jsx
@@ -0,0 +1,170 @@
+import React, { useState } from 'react';
+import { connect } from 'react-redux';
+import propTypes from 'proptypes';
+
+import { updateNC } from '../../../redux/reducers/data';
+
+import { COUNCILS } from '../../common/CONSTANTS';
+import Checkbox from '../../common/Checkbox';
+
+const NCSelector = ({
+ updateNCList,
+}) => {
+ const [searchValue, setSearchValue] = useState('');
+ const [filteredCouncilList, setFilteredCouncilList] = useState(COUNCILS);
+ const [selectedCouncilList, setSelectedCouncilList] = useState(
+ COUNCILS.reduce((acc, council) => {
+ acc[council] = false;
+ return acc;
+ }, { all: false }),
+ );
+
+ const selectRowStyle = {
+ margin: '0 0 7px 0',
+ };
+
+ const selectRowTextStyle = {
+ width: '280px',
+ };
+
+ const handleSearch = (e) => {
+ const term = e.target.value;
+ const searchFilter = new RegExp(term, 'i');
+ const searchList = COUNCILS.filter((council) => searchFilter.test(council));
+ setFilteredCouncilList(searchList);
+ setSearchValue(e.target.value);
+ };
+
+ const handleSelectCouncil = (council) => {
+ const newSelectedCouncilList = { ...selectedCouncilList };
+
+ switch (council) {
+ case 'all': {
+ let value = true;
+
+ if (newSelectedCouncilList.all) {
+ newSelectedCouncilList.all = false;
+ value = false;
+ }
+
+ Object.keys(newSelectedCouncilList).forEach((c) => {
+ newSelectedCouncilList[c] = value;
+ });
+ break;
+ }
+ default:
+ newSelectedCouncilList.all = false;
+ newSelectedCouncilList[council] = !newSelectedCouncilList[council];
+ break;
+ }
+
+ const newNCList = Object.keys(newSelectedCouncilList).filter((c) => newSelectedCouncilList[c] && c !== 'all');
+
+ setSelectedCouncilList(newSelectedCouncilList);
+ updateNCList(newNCList);
+ };
+
+ return (
+
+
+
+
+ Neighborhood Council (NC) Selection
+
+
+
+
+
+
+
+
+
+
+
+
+ handleSelectCouncil('all')}
+ checked={selectedCouncilList?.all ?? false}
+ />
+
+
+
+
+ {filteredCouncilList.map((council) => (
+
+
+
+
+ handleSelectCouncil(council)}
+ checked={selectedCouncilList?.[council] ?? false}
+ />
+
+
+
+ ))}
+
+
+
+ );
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ updateNCList: (council) => dispatch(updateNC(council)),
+});
+
+NCSelector.propTypes = {
+ updateNCList: propTypes.func,
+};
+
+NCSelector.defaultProps = {
+ updateNCList: () => null,
+};
+
+export default connect(null, mapDispatchToProps)(NCSelector);
diff --git a/src/components/main/menu/RequestTypeSelector.jsx b/src/components/main/menu/RequestTypeSelector.jsx
new file mode 100644
index 000000000..e0e221c67
--- /dev/null
+++ b/src/components/main/menu/RequestTypeSelector.jsx
@@ -0,0 +1,191 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'proptypes';
+import {
+ updateRequestType,
+ selectAllRequestTypes,
+ deselectAllRequestTypes,
+} from '../../../redux/reducers/data';
+
+import Checkbox from '../../common/Checkbox';
+import Icon from '../../common/Icon';
+
+import { REQUEST_TYPES } from '../../common/CONSTANTS';
+import COLORS from '../../../styles/COLORS';
+
+const typeContainerStyle = {
+ padding: '2px 0 2px 0',
+ fontSize: '14px',
+};
+
+const checkboxStyle = {
+ display: 'inline-block',
+ paddingRight: '3px',
+ paddingLeft: '3px',
+};
+
+const midIndex = ((list) => {
+ if (list.length / 2 === 0) {
+ return (list.length / 2);
+ }
+ return Math.floor(list.length / 2);
+})(REQUEST_TYPES);
+
+const leftColumnItems = REQUEST_TYPES.slice(0, midIndex);
+const rightColumnItems = REQUEST_TYPES.slice(midIndex);
+
+const RequestItem = ({
+ type,
+ abbrev,
+ selected,
+ color,
+ handleClick,
+}) => (
+
+
+
+
+
+
+ {`${type} [${abbrev}]`}
+
+
+);
+
+const RequestTypeSelector = ({
+ requestTypes,
+ selectType,
+ selectAll,
+ deselectAll,
+}) => {
+ const handleItemClick = (e) => {
+ const type = e.target.value;
+ selectType(type);
+ };
+
+ const renderRequestItems = (items) => items.map((item) => (
+
+ ));
+
+ return (
+
+ {/* ---------- Title ---------- */}
+
+
+ Request Type Selection
+
+
+
+
+
+ {/* ---------- Select/Deselect All ---------- */}
+
+
+
+
+
+ Select/Deselect All
+
+
+ { renderRequestItems(leftColumnItems) }
+
+
+ { renderRequestItems(rightColumnItems) }
+
+
+
+ );
+};
+
+const mapStateToProps = (state) => ({
+ requestTypes: state.data.requestTypes,
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ selectType: (type) => dispatch(updateRequestType(type)),
+ selectAll: () => dispatch(selectAllRequestTypes()),
+ deselectAll: () => dispatch(deselectAllRequestTypes()),
+});
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(RequestTypeSelector);
+
+RequestItem.propTypes = {
+ type: PropTypes.string,
+ abbrev: PropTypes.string,
+ color: PropTypes.string,
+ selected: PropTypes.bool,
+ handleClick: PropTypes.func,
+};
+
+RequestItem.defaultProps = {
+ type: null,
+ abbrev: null,
+ color: null,
+ selected: false,
+ handleClick: () => null,
+};
+
+RequestTypeSelector.propTypes = {
+ requestTypes: PropTypes.shape({
+ All: PropTypes.bool,
+ }),
+ selectType: PropTypes.func,
+ selectAll: PropTypes.func,
+ deselectAll: PropTypes.func,
+};
+
+RequestTypeSelector.defaultProps = {
+ requestTypes: null,
+ selectType: () => null,
+ selectAll: () => null,
+ deselectAll: () => null,
+};
diff --git a/src/index.js b/src/index.js
index 18041d949..b1e87f314 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,9 +3,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
-import 'bulma';
-import 'bulma-checkradio';
-import 'bulma-switch';
+import './styles/styles.scss';
import store from './redux/store';
import App from './App';
diff --git a/src/redux/reducers/data.js b/src/redux/reducers/data.js
index 5d3838f27..83a69cf4a 100644
--- a/src/redux/reducers/data.js
+++ b/src/redux/reducers/data.js
@@ -1,26 +1,22 @@
// import axios from 'axios';
const types = {
- UPDATE_YEAR: 'UPDATE_YEAR',
- UPDATE_START_MONTH: 'UPDATE_START_MONTH',
- UPDATE_END_MONTH: 'UPDATE_END_MONTH',
+ UPDATE_START_DATE: 'UPDATE_START_DATE',
+ UPDATE_END_DATE: 'UPDATE_END_DATE',
UPDATE_REQUEST_TYPE: 'UPDATE_REQUEST_TYPE',
UPDATE_NEIGHBORHOOD_COUNCIL: 'UPDATE_NEIGHBORHOOD_COUNCIL',
+ SELECT_ALL_REQUEST_TYPES: 'SELECT_ALL_REQUEST_TYPES',
+ DESELECT_ALL_REQUEST_TYPES: 'DESELECT_ALL_REQUEST_TYPES',
};
-export const updateYear = (year) => ({
- type: types.UPDATE_YEAR,
- payload: year,
+export const updateStartDate = (newStartDate) => ({
+ type: types.UPDATE_START_DATE,
+ payload: newStartDate,
});
-export const updateStartMonth = (startMonth) => ({
- type: types.UPDATE_START_MONTH,
- payload: startMonth,
-});
-
-export const updateEndMonth = (endMonth) => ({
- type: types.UPDATE_END_MONTH,
- payload: endMonth,
+export const updateEndDate = (newEndDate) => ({
+ type: types.UPDATE_END_DATE,
+ payload: newEndDate,
});
export const updateRequestType = (requestType) => ({
@@ -28,45 +24,91 @@ export const updateRequestType = (requestType) => ({
payload: requestType,
});
-export const updateNeighborhoodCouncil = (council) => ({
+export const selectAllRequestTypes = () => ({
+ type: types.SELECT_ALL_REQUEST_TYPES,
+});
+
+export const deselectAllRequestTypes = () => ({
+ type: types.DESELECT_ALL_REQUEST_TYPES,
+});
+
+export const updateNC = (council) => ({
type: types.UPDATE_NEIGHBORHOOD_COUNCIL,
payload: council,
});
const initialState = {
- year: '2015',
- startMonth: '1',
- endMonth: '12',
- requestType: 'Bulky Items',
- council: null,
+ startDate: null,
+ endDate: null,
+ councils: [],
+ requestTypes: {
+ All: false,
+ 'Dead Animal': false,
+ 'Homeless Encampment': false,
+ 'Single Streetlight': false,
+ 'Multiple Streetlight': false,
+ 'Bulky Items': false,
+ 'E-Waste': false,
+ 'Metal/Household Appliances': false,
+ 'Illegal Dumping': false,
+ Graffiti: false,
+ Feedback: false,
+ Other: false,
+ },
+};
+
+const allRequestTypes = {
+ All: true,
+ 'Dead Animal': true,
+ 'Homeless Encampment': true,
+ 'Single Streetlight': true,
+ 'Multiple Streetlight': true,
+ 'Bulky Items': true,
+ 'E-Waste': true,
+ 'Metal/Household Appliances': true,
+ 'Illegal Dumping': true,
+ Graffiti: true,
+ Feedback: true,
+ Other: true,
};
export default (state = initialState, action) => {
switch (action.type) {
- case types.UPDATE_YEAR:
+ case types.UPDATE_START_DATE: {
return {
...state,
- year: action.payload,
+ startDate: action.payload,
};
- case types.UPDATE_START_MONTH:
+ }
+ case types.UPDATE_END_DATE: {
return {
...state,
- startMonth: action.payload,
+ endDate: action.payload,
};
- case types.UPDATE_END_MONTH:
+ }
+ case types.UPDATE_REQUEST_TYPE:
return {
...state,
- endMonth: action.payload,
+ requestTypes: {
+ ...state.requestTypes,
+ // Flips boolean value for selected request type
+ [action.payload]: !state.requestTypes[action.payload],
+ },
};
- case types.UPDATE_REQUEST_TYPE:
+ case types.SELECT_ALL_REQUEST_TYPES:
+ return {
+ ...state,
+ requestTypes: allRequestTypes,
+ };
+ case types.DESELECT_ALL_REQUEST_TYPES:
return {
...state,
- requestType: action.payload,
+ requestTypes: initialState.requestTypes
};
case types.UPDATE_NEIGHBORHOOD_COUNCIL:
return {
...state,
- council: action.payload,
+ councils: action.payload,
};
default:
return state;
diff --git a/src/styles/styles.scss b/src/styles/styles.scss
new file mode 100644
index 000000000..10a601075
--- /dev/null
+++ b/src/styles/styles.scss
@@ -0,0 +1,19 @@
+@charset "utf-8";
+
+/*
+* Define new sass variables or overwrite Bulma's default variables before the imports.
+* See https://bulma.io/documentation/customize/variables/ and component-specific
+* Bulma docs for more variables.
+*
+* Example:
+* $primary: #002449;
+* $dropdown-item-color: $primary;
+*/
+
+
+
+
+// Additional sass sheets or Bulma extension libraries used should be added to the imports below.
+@import "~bulma/bulma.sass";
+@import "~bulma-checkradio/dist/css/bulma-checkradio.sass";
+@import "~bulma-switch/dist/css/bulma-switch.sass";
diff --git a/webpack.config.js b/webpack.config.js
index e4e38b333..65c2ea059 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,5 +1,6 @@
const Dotenv = require('dotenv-webpack');
-const HtmlWebpackPlugin = require('html-webpack-plugin')
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
module.exports = {
@@ -24,9 +25,17 @@ module.exports = {
{
test: /\.(css|scss|sass)$/,
use: [
- 'style-loader',
- 'css-loader',
- 'sass-loader',
+ MiniCssExtractPlugin.loader,
+ {
+ loader: 'css-loader',
+ },
+ {
+ loader: 'sass-loader',
+ options: {
+ sourceMap: true,
+ // options
+ },
+ },
],
},
{
@@ -40,8 +49,11 @@ module.exports = {
plugins: [
new Dotenv(),
new HtmlWebpackPlugin({
- template: "./public/index.html",
- title: "311-Data"
- })
+ template: './public/index.html',
+ title: '311-Data',
+ }),
+ new MiniCssExtractPlugin({
+ filename: 'css/styles.css',
+ }),
],
};