diff --git a/src/components/App.js b/src/components/App.js index dc7f898..ff122f0 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -4,6 +4,7 @@ import moment from 'moment'; import Panel from './Panel'; import Geocoder from './Geocoder'; import Cal from './Cal'; +import { buildWasteAPI } from '../utils/WasteAPI'; import './App.scss'; import '../../node_modules/leaflet/dist/leaflet.css'; @@ -11,21 +12,13 @@ export default class App { constructor() { this.month = moment().month() + 1; this.year = moment().year(); - this.schedule = { - garbage: null, - recycle: null, - bulk: null, - yard: { - start: null, - end: null - } - } this.point = null; this.map = null; this.layers = {}; this.calendar = new Cal('calendar', this);; this.panel = new Panel(this); this.geocoder = new Geocoder('geocoder', this); + this.routeNum = null; this.initialLoad(this); } @@ -102,20 +95,11 @@ export default class App { } _app.map.flyTo(tempLocation, 15); _app.panel.currentProvider = featureCollection.features[0].properties.contractor; - fetch(`https://apis.detroitmi.gov/waste_schedule/details/${featureCollection.features[0].properties.FID}/year/${_app.year}/month/${_app.month}/`) + _app.routeNum = featureCollection.features[0].properties.FID; + const wasteAPIEndpoint = buildWasteAPI(_app.routeNum, _app.year, _app.month); + fetch(wasteAPIEndpoint) .then((res) => { res.json().then(data => { - data.details.forEach((d)=>{ - if(d.type == 'start-date' && d.service == 'yard waste'){ - _app.schedule.yard.start = d.newDay; - } - if(d.type == 'end-date' && d.service == 'yard waste'){ - _app.schedule.yard.end = d.newDay; - } - }); - _app.schedule.garbage = data.next_pickups.trash.next_pickup; - _app.schedule.recycle = data.next_pickups.recycling.next_pickup; - _app.schedule.bulk = data.next_pickups.bulk.next_pickup; _app.panel.location.lat = tempLocation.lat; _app.panel.location.lng = tempLocation.lng; _app.panel.data = data; diff --git a/src/components/Cal.js b/src/components/Cal.js index 2cc199d..d68e2af 100644 --- a/src/components/Cal.js +++ b/src/components/Cal.js @@ -1,12 +1,12 @@ import { Calendar } from '@fullcalendar/core'; import dayGridPlugin from '@fullcalendar/daygrid'; import moment from 'moment'; +import { buildWasteAPI, PICKUP_TYPES, PICKUP_TYPES_PRINT } from '../utils/WasteAPI'; import './Cal.scss'; export default class Cal { constructor(container, _controller) { this.calendar = null; this.controller = _controller; - this.pickups = []; } createCalendar(_app){ @@ -22,7 +22,7 @@ export default class Cal { calContainer.id = 'calendar'; tempCal.innerHTML = `
- Garbage + Garbage Recycle Bulk Yard @@ -30,131 +30,116 @@ export default class Cal { `; tempCal.appendChild(calContainer); tempCal.prepend(closeBtn); - _app.calendar.buildSchedule(_app); _app.calendar.calendar = new Calendar(calContainer, { plugins: [ dayGridPlugin ], - eventSources: _app.calendar.pickups + showNonCurrentDates: false, + eventSources: [ + { + events: this.fetchTrashPickups.bind(this), + color: '#cb4d4f', + textColor: 'white' + }, + { + events: this.fetchRecyclingPickups.bind(this), + color: '#9FD5B3', + textColor: '#004445' + }, + { + events: this.fetchBulkPickups.bind(this), + color: '#5f355a', + textColor: 'white' + }, + { + events: this.fetchYardPickups.bind(this), + color: '#feb70d', + textColor: 'black' + } + ], }); _app.calendar.calendar.render(); document.querySelector('#app .calendar').className = "calendar active"; } - buildSchedule(_app){ - _app.calendar.buildPickUps(_app); + fetchPickups(info, successCb, failureCb, pickupType, routeNum, eventBuilder) { + const month = info.start.getMonth() + 1; + const wasteAPIEndpoint = buildWasteAPI(routeNum, info.start.getFullYear(), month); + fetch(wasteAPIEndpoint, {'cache': 'force-cache'}) + .then((res) => { + res.json().then((data) => { + const events = eventBuilder(data, pickupType); + successCb(events); + }) + }) + .catch((error) => { + console.error(error); + failureCb(error); + }); } - buildPickUps(_app){ - let pastDate = _app.schedule.recycle; - let latestDate = _app.schedule.recycle; - let pastTrashDate = _app.schedule.garbage; - let trashDate = _app.schedule.garbage; + fetchTrashPickups(info, successCb, failureCb) { + this.fetchPickups( + info, + successCb, + failureCb, + PICKUP_TYPES.TRASH, + this.controller.routeNum, + this.buildPickUps + ); + } - let gList = { - events: [ - { - title : 'Trash', - start : moment(trashDate).format('YYYY-MM-DD'), - } - ], - color: '#cb4d4f', // an option! - textColor: 'white' // an option! - }; - let rList = { - events: [ - { - title : 'Recycle', - start : moment(latestDate).format('YYYY-MM-DD'), - } - ], - color: '#9FD5B3', // an option! - textColor: '#004445' // an option! - }; - let bList = { - events: [ - { - title : 'Bulk', - start : moment(latestDate).format('YYYY-MM-DD'), - } - ], - color: '#5f355a', // an option! - textColor: 'white' // an option! - }; - let yList = { - events: [], - color: '#feb70d', // an option! - textColor: 'black' // an option! - } - if(moment(latestDate).isBetween(_app.schedule.yard.start, _app.schedule.yard.end)){ - yList.events.push( - { - title : 'Yard', - start : moment(latestDate).format('YYYY-MM-DD'), - } - ); - } - for (let index = 0; index < 52; index++) { - let tempG = { - title : 'Trash', - start : moment(trashDate).add(7,'d').format('YYYY-MM-DD'), - }; - let pasttempG = { - title : 'Trash', - start : moment(pastTrashDate).subtract(7,'d').format('YYYY-MM-DD'), - }; - trashDate = moment(trashDate).add(7,'d'); - pastTrashDate = moment(pastTrashDate).subtract(7,'d'); - gList.events.push(tempG); - gList.events.push(pasttempG); - } + fetchRecyclingPickups(info, successCb, failureCb) { + this.fetchPickups( + info, + successCb, + failureCb, + PICKUP_TYPES.RECYCLING, + this.controller.routeNum, + this.buildPickUps + ); + } - for (let index = 0; index < 26; index++) { - let tempR = { - title : 'Recycle', - start : moment(latestDate).add(14,'d').format('YYYY-MM-DD'), - }; - let tempB = { - title : 'Bulk', - start : moment(latestDate).add(14,'d').format('YYYY-MM-DD'), - }; - if(moment(latestDate).add(14,'d').isBetween(_app.schedule.yard.start, _app.schedule.yard.end)){ - let tempY = { - title : 'Yard', - start : moment(latestDate).add(14,'d').format('YYYY-MM-DD'), - }; - yList.events.push(tempY); - } - let pasttempR = { - title : 'Recycle', - start : moment(pastDate).subtract(14,'d').format('YYYY-MM-DD'), - }; - let pasttempB = { - title : 'Bulk', - start : moment(pastDate).subtract(14,'d').format('YYYY-MM-DD'), - }; - if(moment(pastDate).subtract(14,'d').isBetween(_app.schedule.yard.start, _app.schedule.yard.end)){ - let pasttempY = { - title : 'Yard', - start : moment(pastDate).subtract(14,'d').format('YYYY-MM-DD'), - }; - yList.events.push(pasttempY); + fetchYardPickups(info, successCb, failureCb) { + this.fetchPickups( + info, + successCb, + failureCb, + PICKUP_TYPES.YARD_WASTE, + this.controller.routeNum, + this.buildPickUps + ); + } + + fetchBulkPickups(info, successCb, failureCb) { + this.fetchPickups( + info, + successCb, + failureCb, + PICKUP_TYPES.BULK, + this.controller.routeNum, + this.buildPickUps + ); + } + + buildPickUps(eventData, pickupType){ + let events = []; + eventData.schedule.forEach((pickupDateDetails) => { + for (const [pickupDate, pickupTypeDetails] of Object.entries(pickupDateDetails)) { + if (pickupType in pickupTypeDetails) { + events.push( + { + title : PICKUP_TYPES_PRINT[pickupType], + start : moment(pickupDate).format('YYYY-MM-DD'), + } + ); + } } - pastDate = moment(pastDate).subtract(14,'d'); - latestDate = moment(latestDate).add(14,'d'); - rList.events.push(tempR); - bList.events.push(tempB); - rList.events.push(pasttempR); - bList.events.push(pasttempB); - } - _app.calendar.pickups.push(gList); - _app.calendar.pickups.push(rList); - _app.calendar.pickups.push(bList); - _app.calendar.pickups.push(yList); + }); + return events; } closeCalendar(ev,_calendar){ _calendar.calendar.destroy(); _calendar.calendar = null; - _calendar.pickups.length = 0; let tempClass = ev.target.parentNode.parentNode.className; tempClass = tempClass.split(' '); ev.target.parentNode.parentNode.className = tempClass[0]; diff --git a/src/components/Panel.js b/src/components/Panel.js index 4976f98..b700e98 100644 --- a/src/components/Panel.js +++ b/src/components/Panel.js @@ -122,18 +122,8 @@ export default class Panel { document.querySelector('#app .panel').className = "panel active"; } - checkRecyclingStatus(_panel){ - if(_panel.data.next_pickups['yard waste']){ - if(moment(_panel.data.next_pickups['yard waste'].next_pickup).isBetween(_panel.app.schedule.yard.start, _panel.app.schedule.yard.end)){ - return true; - }else{ - return false; - } - }else{ - return false; - } - } - + // TODO: Stop using next_pickups from API: + // https://github.com/CityOfDetroit/dpw-map/issues/53 buildNextPickup(_panel){ return ` @@ -155,7 +145,7 @@ export default class Panel { BULK

${moment(_panel.data.next_pickups.bulk.next_pickup).format('dddd - MMM Do')}

- ${(_panel.checkRecyclingStatus(_panel)) ? ` + ${('yard waste' in _panel.data.next_pickups) ? `
YARD

${moment(_panel.data.next_pickups['yard waste'].next_pickup).format('dddd - MMM Do')}

diff --git a/src/utils/WasteAPI.js b/src/utils/WasteAPI.js new file mode 100644 index 0000000..81626ce --- /dev/null +++ b/src/utils/WasteAPI.js @@ -0,0 +1,26 @@ +/** + * Returns the API endpoint for the City of Detroit Waste Schedule API. + * @param {number} routeNum - The number denoting the waste pickup route. + * @param {string} year - The year formatted as YYYY. + * @param {string} month - The month formatted as MM and 1-indexed. + * @returns {string} - A REST API endpoint URL. + */ +function buildWasteAPI(routeNum, year, month) { + return `https://apis.detroitmi.gov/waste_schedule/details/${routeNum}/year/${year}/month/${month}/`; +} + +const PICKUP_TYPES = Object.freeze({ + TRASH: "trash", + RECYCLING: "recycling", + BULK: "bulk", + YARD_WASTE: "yard waste" +}); + +const PICKUP_TYPES_PRINT = Object.freeze({ + "trash": "Garbage", + "recycling": "Recycle", + "bulk": "Bulk", + "yard waste": "Yard" +}); + +export {buildWasteAPI, PICKUP_TYPES, PICKUP_TYPES_PRINT}; \ No newline at end of file