diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 951ab90..1abbd47 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -15,27 +15,27 @@ The items listed under **Your Repository** and **Core Interface Elements** are a ### Layout -* [ ] Unless otherwise specified, all features should be useable on both small (mobile) and large (laptop/desktop) screens. **Your HTML document should have appropriate `meta` tag(s) to make this possible.** +* [x] Unless otherwise specified, all features should be useable on both small (mobile) and large (laptop/desktop) screens. **Your HTML document should have appropriate `meta` tag(s) to make this possible.** ### Loading voter files... -* [ ] There should be an `input` element on the page where you can enter a voter file number. **Save the `input` DOM element in a variable named `voterFileInput` attached to the global `window` object.** In other words: +* [x] There should be an `input` element on the page where you can enter a voter file number. **Save the `input` DOM element in a variable named `voterFileInput` attached to the global `window` object.** In other words: ```js window.voterFileInput = ...; ``` -* [ ] There should be a `button` that will load the voter file number given in the `voterFileInput` when clicked. **Save the `button` DOM element in a variable named `voterFileLoadButton` attached to the global `window` object.** +* [x] There should be a `button` that will load the voter file number given in the `voterFileInput` when clicked. **Save the `button` DOM element in a variable named `voterFileLoadButton` attached to the global `window` object.** ### Listing and mapping voters... -* [ ] Your page should have an element that shows the voters (and their addresses) from a file. Note that the element _does not_ need to be a `ul`, `ol`, or any other HTML list element. Even though the element itself may not use a list tag like `ul` or `ol`, it will still function as a list and I'll still refer to it as one. **The list's DOM element should be available on the global `window` object as a variable named `voterList`.** +* [x] Your page should have an element that shows the voters (and their addresses) from a file. Note that the element _does not_ need to be a `ul`, `ol`, or any other HTML list element. Even though the element itself may not use a list tag like `ul` or `ol`, it will still function as a list and I'll still refer to it as one. **The list's DOM element should be available on the global `window` object as a variable named `voterList`.** -* [ ] Your page should have a Leaflet map to show voter locations. **The Leaflet map object should be available on the global `window` object as a variable named `voterMap`.** +* [x] Your page should have a Leaflet map to show voter locations. **The Leaflet map object should be available on the global `window` object as a variable named `voterMap`.** * When you enter a file number, the voter information in that CSV file should be loaded onto the `voterMap` and into the `voterList`. - * [ ] **Wrap each voter's name in an element (for example a `span`) with the class `voter-name`. Wrap addresses in an element with the class `voter-address`** You may choose to list each voter individually or grouped by address, which I would recommend. Either way, each voter's basic information (at least their name and street address) should be shown in the `voterList`. - * [ ] **Represent the voters in the file with map markers.** You may choose to have one map marker to represent each voter, one marker to represent each address, or one marker to represent each _building_ (for example, two apartments that share the same street address are in the same building). I would generally recommend showing a marker for each building, as otherwise markers for different apartments or voters in the same building will be overlapping. + * [x] **Wrap each voter's name in an element (for example a `span`) with the class `voter-name`. Wrap addresses in an element with the class `voter-address`** You may choose to list each voter individually or grouped by address, which I would recommend. Either way, each voter's basic information (at least their name and street address) should be shown in the `voterList`. + * [x] **Represent the voters in the file with map markers.** You may choose to have one map marker to represent each voter, one marker to represent each address, or one marker to represent each _building_ (for example, two apartments that share the same street address are in the same building). I would generally recommend showing a marker for each building, as otherwise markers for different apartments or voters in the same building will be overlapping. * [ ] When you click on a map marker, the marker should be highlighted in some way to show that it is selected. **Change the marker styles of a selected marker if it is a vector marker (e.g. `L.circleMarker`), or change the icon if it is a normal image marker (e.g. `L.marker`).** @@ -45,7 +45,7 @@ The items listed under **Your Repository** and **Core Interface Elements** are a > _Note that if you decide to implement a workflow that doesn't precisely fit into the structure below, that's ok! Just talk with me about what the workflow is, because we may need to modify the project tests._ -* [ ] When you click on a voter (or an address) in the `voterList`, a panel should be shown that contains details about the voter (or about each voter at the address). This panel could be represented in HTML with a `div`, `form`, `section`, or any of a number of other elements. **Give the voter information panel(s) a class of `voter-details`.** +* [x] When you click on a voter (or an address) in the `voterList`, a panel should be shown that contains details about the voter (or about each voter at the address). This panel could be represented in HTML with a `div`, `form`, `section`, or any of a number of other elements. **Give the voter information panel(s) a class of `voter-details`.** * [ ] There should be _at least_ three separate input elements available for collecting facts about each voter (refer to the [product requirements document](PRD.md) that we created in class to remind yourself what kind of information should be collected). **Include fields for collecting voter information on each `voter-details` panel.** diff --git a/package-lock.json b/package-lock.json index 02ef655..ed7b5dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "school-explorer", "version": "1.0.0", "license": "ISC", + "dependencies": { + "papaparse": "^5.3.2" + }, "devDependencies": { "eslint": "^8.22.0", "http-server": "^14.1.1", @@ -4870,6 +4873,11 @@ "node": ">=6" } }, + "node_modules/papaparse": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.2.tgz", + "integrity": "sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10372,6 +10380,11 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "papaparse": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.3.2.tgz", + "integrity": "sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/package.json b/package.json index 3a52e5f..b70795f 100644 --- a/package.json +++ b/package.json @@ -27,5 +27,8 @@ "jest-puppeteer": "^6.1.1", "stylelint": "^14.11.0", "stylelint-config-standard": "^28.0.0" + }, + "dependencies": { + "papaparse": "^5.3.2" } } diff --git a/site/css/styles.css b/site/css/styles.css new file mode 100644 index 0000000..7668e3e --- /dev/null +++ b/site/css/styles.css @@ -0,0 +1,132 @@ +@import "https://fonts.googleapis.com/css2?family=Droid+Sans"; + +html { + font-family: "Droid Sans", sans-serif; +} + +h1 { + background-color: #364477; + color: whitesmoke; + margin: 0px; + padding: 12px; + text-align: center; +} + +h2 { + background-color: #364477; + color: whitesmoke; + margin: 0px; + padding: 5px; + text-align: center; + font-size: 15px; +} + +body { + display: flex; + flex-direction: column; + height: 100vh; + margin: 0; + padding: 0; + z-index: 1; +} + +.map { + height: 100%; + z-index: 1; +} + +textarea { + text-align: center; + height: 20%; + + display: block; + margin-top: 0.25rem; + margin-bottom: 0.25rem; + margin-right: 10rem; + height: 2rem; + z-index: -1; +} + +.voter-list-container { + width: 100%; + display: flex; + flex-direction: row; + flex-wrap: wrap; + height: 10rem; + overflow-y: scroll; + overflow-x: auto; + z-index: 1; +} + +#response-container { + background-color: #364477; + position: absolute; + top: 0%; + left: 65%; + right: 20%; + margin: 5px; + padding: 5px; + border-radius: 15px; + border: 40px #364477; + height: auto; + width: 300px; + flex-wrap: wrap; + flex-direction: row-reverse; + justify-content: center; + align-items: center; + z-index: 1; + font-size: 10px; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: azure; + transform: inherit; + +} + +#voter-notes { + display: flex; + height: 300px; + width: 300px; + flex-direction: column; + align-items: center; + margin-top: 0.25rem; +} + +button { + align-self: center; + margin-top: 0.25rem; + margin-bottom: 0.25rem; + margin-right: 10rem; + text-align: left; + display: flex; + flex-direction: row; + flex-wrap: wrap; + font-size: 15px; + background-color: #364477; + color: white; +} + +h3 { + top: 10%; +} + +.voter-address { + font-style: normal; + font-weight: bold; + font-size: 5px; +} + +#voter-list { + background-color: #364477; + color: #ffffff; + text-align: center; + width: 100%; + display: flex; + flex-direction: column; + flex-wrap: wrap; + margin-bottom: 0.5rem; +} + +#blank { + height: auto; + width: auto; +} \ No newline at end of file diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..9190885 --- /dev/null +++ b/site/index.html @@ -0,0 +1,55 @@ + + + + + + + + + Voter Canvassing + + + + + + + +

Voter Canvassing App

+
+ +
+ +
+
  • +
    + +
    + + + + +
    + +
    + + +
    +

    +

    +

    +
    + + +
    +
    + + + + + + + + + \ No newline at end of file diff --git a/site/js/inventory.js b/site/js/inventory.js new file mode 100644 index 0000000..ee32a2c --- /dev/null +++ b/site/js/inventory.js @@ -0,0 +1,22 @@ +function saveNote(voterID, content, app){ + app.notes[voterID] = content; + + localStorage.setItem('notes', JSON.stringify(app.notes)); +} + +function loadNotes(onSuccess, onFailure){ + try{ + const notes = JSON.parse(localStorage.getItem('notes')); + onSuccess(notes); + } catch { + alert('Oh no, We failed'); + if (onFailure) {onFailure()} + } + + +} + +export{ + saveNote, + loadNotes, +} \ No newline at end of file diff --git a/site/js/main.js b/site/js/main.js new file mode 100644 index 0000000..70ca99f --- /dev/null +++ b/site/js/main.js @@ -0,0 +1,298 @@ +import { loadNotes, saveNote } from './inventory.js'; +import { initMap } from './map.js'; +import { getFormContent, showVoterdata } from './voter-info-form.js'; +import { voterMenuList } from './voterlist.js'; + +let map = initMap(); +let voterList; +let data_json; + +let app = { + currentVoter: null, + notes: {}, +} + +let listNum + +let voterFileInput = document.querySelector('.input'); +let voterFileLoadButton = document.querySelector('#load-voter-list-button'); +let voterListObj = document.querySelector("#voter-list"); +let clearMapButton = document.querySelector('#clear-map-button'); +let clearInputTextButton = document.querySelector('#clear-text-button'); + + + +//Functions + + +// Click on button for markers and list to appear function +function onButtonClicked() { + listNum = voterFileInput.value; + downloadInventory(makeVoterFeature); + voterMenuList(data_json, voterListObj); +} + + +// Click on button to clear map function +function onClearMapButtonClicked() { + map.removeLayer(map.voterLayer); + voterListObj.innerHTML = ``; +} + +function clearMap() { + clearMapButton.addEventListener('click', onClearMapButtonClicked); +} +clearMap(); + + +// Click on button to clear input function +function onClearInputButtonClicked() { + voterFileInput.value = ''; +} + +function clearInputTextBox() { + clearInputTextButton.addEventListener('click', onClearInputButtonClicked); +} +clearInputTextBox(); + + +// Click on button to load Voter List +function loadVoterListClick() { + voterFileLoadButton.addEventListener('click', onButtonClicked); +} +loadVoterListClick(); + +function loadVoterListEnter() { + voterFileInput.addEventListener('keypress', function (event) { + if (event.keyCode == 13) + voterFileLoadButton.click(); + }); +} +loadVoterListEnter(); + + +// Creating Voter List +/*function makeVoterList(data_json) { + voterList = []; + const voter = { + type: "FeatureCollection", + features: [], + }; + let i; + console.log(data_json); + console.log(data_json.length); + for (i = 0; i < data_json.length; i++) { + voterList.features.push({ + "type": "Feature", + "properties": { + "name" : data_json[i]["First Name"].concat(" ", data_json[i]["Last Name"]), + "address": data_json[i]["TIGER/Line Matched Address"], + }, + }); + } + console.log(voter); + } + onInventoryLoadSuccess(voter); + return voterList; + }*/ + +// Downloading data from /data/voters_lists and converting to json. +async function downloadInventory(onSuccess, onFailure) { + const resp = await fetch('data/voters_lists/' + listNum + '.csv'); + if (resp.status === 200) { + const data = await resp.text(); + data_json = Papa.parse(data, { header: true, skipEmptyLines: 'greedy' }).data; + if (onSuccess) { onSuccess(data_json) } + //console.log(data_json) + } else { + alert('Oh no, I failed to download the data.'); + if (onFailure) { onFailure() } + } +}; + +//Function to show voter markers on map +function onInventoryLoadSuccess(voters) { + voterstoshow(voters); + map.voterLayer.addData(voters); +} + +//Function to choose Philly coords +function coordsPhilly(lng, lat) { + let result = false; + if (typeof (lng) == "number" && typeof (lat) == "number") { + if (lng < -73 && lng > -77 && lat < 41 && lat > 38) { + result = true; + } + } + return result; +} + +//Function to create voter properties +function makeVoterFeature(data_json) { + const voters = { + type: "FeatureCollection", + features: [], + }; + + + let i; + //console.log(data_json); + //console.log(data_json.length); + for (i = 0; i < data_json.length; i++) { + let LatLng = data_json[i]["TIGER/Line Lng/Lat"]; + if (typeof (LatLng) == "string") { + + let Lng = Number(LatLng.split(",")[0]); + let Lat = Number(LatLng.split(",")[1]); + + if (coordsPhilly(Lng, Lat)) { + voters.features.push({ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [Lng, Lat], + }, + "properties": { + "name": data_json[i]["First Name"].concat(" ", data_json[i]["Last Name"]), + "id": data_json[i]["ID Number"], + "last_name": data_json[i]["Last Name"], + "first_name": data_json[i]["First Name"], + "address": data_json[i]["TIGER/Line Matched Address"], + "VotingParty": data_json[i]["Party Code"], + "Status": data_json[i]["Voter Status"], + "languageAssistance": "", + }, + + }); + } + } + //console.log(voters); + } + onInventoryLoadSuccess(voters); +} + + +//Function to create voter markers +function voterstoshow(data_json) { + if (map.voterLayer !== undefined) { + map.removeLayer(map.voterLayer); + } + + map.voterLayer = L.geoJSON(null, { + pointToLayer: (geoJsonPoint, latlng) => L.circleMarker(latlng), + style: { + fillColor: '#ffffff', + fillOpacity: 1, + stroke: true, + color: '#00008B', + radius: 5, + weight: 2.5 + }, + }) + .bindTooltip(layer => layer.feature.properties['name']) + .addTo(map); + + + setupinteractionevents(); + /*map.setView([data_json["features"][0]["geometry"]["coordinates"][1], data_json["features"][0]["geometry"]["coordinates"][0]], 16);*/ +} + +/* +function onvoterSelected(evt) { + const voter = evt.layer.feature; + app.currentVoter = voter; + showVoterdata(voter, app); + } +*/ + + +/* +function voterhighlight(voter){ + map.layer = L.geoJSON(null, { + pointToLayer: (geoJsonPoint, latlng) => L.circleMarker(latlng), + style: { + fillColor: '#ff0000', + fillOpacity: 0.6, + stroke: false, + }, + }).addTo(map); +} +*/ + +const highlight = { + fillColor: '#ff0000', + fillOpacity: 0.6, + stroke: false, +}; + + + + +const saveVoterNotesEl = document.getElementById('save-notes'); + + + + +function onvoterSelected(evt) { + let voterCard = document.createElement("div"); + const voterhighlight = evt.layer.feature; + if (map.layer !== undefined) { + map.removeLayer(map.layer); + } + map.layer = L.geoJSON(voterhighlight, { + pointToLayer: function (feature, latlng) { + return L.circleMarker(latlng, highlight); + } + }).addTo(map); + + + //map.layer = L.circleMarker(voterhighlight).addTo(map); + const voter = evt.layer.feature.properties; + //voterhighlight(voterhighlight); + app.currentVoter = voter; + + showVoterdata(voter, app); +} + + +function onSaveClicked() { + const content = getFormContent(); + const voterID = app.currentVoter['id']; + saveNote(voterID, content, app); +} + +function setupinteractionevents() { + map.voterLayer.addEventListener('click', onvoterSelected); + saveVoterNotesEl.addEventListener('click', onSaveClicked); + //document.getElementById('click').style.color = "red"; +} + +//Function to show voter data on markers +/*function onvoterSelected(evt) { + const voterMarker = evt.layer.feature.properties; + app.currentVoter = voterMarker; + showVoterdata(voterMarker, app); +}*/ + +/*function setupinteractionevents() { + map.voterLayer.addEventListener('click', onvoterSelected); +}*/ + +setupinteractionevents(); + +function onNotesLoadSucess(notes) { + app.notes = notes; +} + +loadNotes(onNotesLoadSucess); + + + +//Making everything globally available +window.map = map; +window.app = app; +window.voterFileInput = voterFileInput; +window.voterFileLoadButton = voterFileLoadButton; +window.voterListObj = voterListObj; +window.voterList = voterList; + diff --git a/site/js/map.js b/site/js/map.js new file mode 100644 index 0000000..03760bd --- /dev/null +++ b/site/js/map.js @@ -0,0 +1,14 @@ +function initMap () { + let map = L.map('map', { maxZoom: 22, preferCanvas: true }).setView([39.95, -75.16], 11.5); + L.tileLayer(`https://api.mapbox.com/styles/v1/simran-aro-map/clbcy72bk001615nu08334s8v/tiles/256/{z}/{x}/{y}@2x?access_token=pk.eyJ1Ijoic2ltcmFuLWFyby1tYXAiLCJhIjoiY2xhdTJlMm9yMDI1ZTN4cHJvZW51Nno4NCJ9.rcqye_8iI_wAlqbcNVTlog&zoomwheel=true&fresh=true#12.14/40.73311/-73.97065`, { + maxZoom: 22, + attribution: '© Mapbox © OpenStreetMap Improve this map', + }).addTo(map); + return map; + } + + +export { + initMap, +}; + diff --git a/site/js/template-tools.js b/site/js/template-tools.js new file mode 100644 index 0000000..e21812e --- /dev/null +++ b/site/js/template-tools.js @@ -0,0 +1,36 @@ +/* ==================== +The following two functions take a string of HTML and create DOM element objects +representing the tags, using the `template` feature of HTML. See the following +for more information: https://stackoverflow.com/a/35385518/123776 +==================== */ + +/* eslint-disable no-unused-vars */ + +/** + * @param {String} HTML representing a single element + * @return {Element} + */ + function htmlToElement(html) { + const template = document.createElement('template'); + const trimmedHtml = html.trim(); // Never return a text node of whitespace as the result + template.innerHTML = trimmedHtml; + return template.content.firstChild; + } + + /** + * @param {String} HTML representing any number of sibling elements + * @return {NodeList} + */ + function htmlToElements(html) { + const template = document.createElement('template'); + template.innerHTML = html; + return template.content.childNodes; + } + + window.htmlToElement = htmlToElement; + window.htmlToElements = htmlToElements; + + export { + htmlToElement, + htmlToElements, + }; \ No newline at end of file diff --git a/site/js/toast.js b/site/js/toast.js new file mode 100644 index 0000000..e69de29 diff --git a/site/js/voter-info-form.js b/site/js/voter-info-form.js new file mode 100644 index 0000000..680b8a1 --- /dev/null +++ b/site/js/voter-info-form.js @@ -0,0 +1,28 @@ +const voterNameEl = document.querySelector('#Name'); +const voterAddressEl = document.querySelector('#Address'); +const voterPartyEl = document.querySelector('#Voting-Party'); +var VoterNoteEl = document.getElementById('.people-notes'); + +function showVoterdata(voter) { + const Votername = voter['name']; + const VoterAddress = voter['address']; + const VoterParty = voter['VotingParty']; + const voterID = voter['id']; + //const note = app.notes[voterID] || ''; + // const VotingParty = voter['VotingParty']; + //const languageAssistance = voter['languageAssistance'] + voterNameEl.innerHTML = Votername; + voterAddressEl.innerHTML = VoterAddress; + voterPartyEl.innerHTML = VoterParty; +} + +function getFormContent() { + const note = VoterNoteEl.value; + const name = voterNameEl.innerHTML; + return note; +} + +export { + showVoterdata, + getFormContent, +}; \ No newline at end of file diff --git a/site/js/voterlist.js b/site/js/voterlist.js new file mode 100644 index 0000000..ec58adc --- /dev/null +++ b/site/js/voterlist.js @@ -0,0 +1,41 @@ +import { htmlToElement } from "./template-tools.js"; + + + +function voterMenuList(voterList, voterListObj) { + voterListObj.innerHTML = ''; + + for (let v of voterList) { + + const html = ` +
  • Name : ${v['First Name']} ${v['Last Name']}.
    + Address: ${v['TIGER/Line Matched Address']}
  • + `; + + const li = htmlToElement(html); + voterListObj.append(li); + } +} + +export { + voterMenuList, +}; + + +/*import { htmlToElement } from "./template-tools.js"; + +function showSchoolsInList(schoolsToShow, schoolList) { + schoolList.innerHTML = ''; + + for (const school of schoolsToShow) { + const html = ` +
  • ${school['name']}. School Level: ${school['School Level']}
  • + `; + const li = htmlToElement(html); + schoolList.append(li); + } +} + +export { + showSchoolsInList, +};*/ \ No newline at end of file