diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9303c34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +npm-debug.log \ No newline at end of file diff --git a/.sass-cache/044ac856268bb8c1ecdb415330e4a633469d4cdb/style.scssc b/.sass-cache/044ac856268bb8c1ecdb415330e4a633469d4cdb/style.scssc new file mode 100644 index 0000000..a131adb Binary files /dev/null and b/.sass-cache/044ac856268bb8c1ecdb415330e4a633469d4cdb/style.scssc differ diff --git a/README.md b/README.md index cb007d2..e8a0ef8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # project_what_have_you_done Build an application to help track the legislative activities of your local representatives. + +## [View App Here](http://find-my-rep.herokuapp.com/) \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..c317392 --- /dev/null +++ b/app.js @@ -0,0 +1,46 @@ +var express = require('express'); +var path = require('path'); +var favicon = require('serve-favicon'); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); +var bodyParser = require('body-parser'); + +var index = require('./routes/index'); +let legislators = require('./routes/legislators') + +var app = express(); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'hbs'); + +// uncomment after placing your favicon in /public +//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); +app.use(logger('dev')); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/', index); +app.use('/legislators', legislators) + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + var err = new Error('Not Found'); + err.status = 404; + next(err); +}); + +// error handler +app.use(function(err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render('error'); +}); + +module.exports = app; diff --git a/bin/www b/bin/www new file mode 100755 index 0000000..22f988a --- /dev/null +++ b/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('project-what-have-you-done:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/models/legislators.js b/models/legislators.js new file mode 100644 index 0000000..0282d95 --- /dev/null +++ b/models/legislators.js @@ -0,0 +1,14 @@ +class Legistlator { + constructor(id, img, name, chamber, party, phone, website, votes) { + this.id = id + this.img = img + this.name = name + this.chamber = chamber + this.party = party + this.phone = phone + this.website = website + this.votes = votes + } +} + +module.exports = Legistlator \ No newline at end of file diff --git a/models/votes.js b/models/votes.js new file mode 100644 index 0000000..4ce3116 --- /dev/null +++ b/models/votes.js @@ -0,0 +1,11 @@ +class Vote { + constructor(bill_id, official_title, url, question, vote){ + this.bill_id = bill_id + this.official_title = official_title + this.url = url + this.question = question + this.vote = vote + } +} + +module.exports = Vote \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..f9cf5cd --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "project-what-have-you-done", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "node ./bin/www" + }, + "dependencies": { + "body-parser": "~1.17.1", + "cookie-parser": "~1.4.3", + "debug": "~2.6.3", + "express": "~4.15.2", + "hbs": "~4.0.1", + "morgan": "~1.8.1", + "request": "^2.81.0", + "serve-favicon": "~2.4.2" + } +} diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css new file mode 100644 index 0000000..0431468 --- /dev/null +++ b/public/stylesheets/style.css @@ -0,0 +1,59 @@ +body { + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; } + +a { + color: #000; + text-decoration: none; } + a:hover { + color: #000; + text-decoration: none; } + +.results { + display: inline-flex; + width: 100%; } + +.card-container { + text-align: center; } + +.card { + background-color: #fff; + display: inline-flex; + margin: 50px; + width: 300px; + height: 480px; + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); + transition: 0.3s; } + .card:hover { + box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2); } + .card img { + width: 300px; + height: 365px; } + .card .chamber { + text-align: center; + margin: 10px; + color: #aaa; + font-size: 16px; } + .card .website { + color: #8aacb8; } + +.single { + display: inline; } + +.recent-votes { + margin-top: 50px; } + +.inner-card { + padding: 2px 16px; } + +.form-card { + max-width: 500px; + margin: 0 auto; + text-align: center; } + .form-card form { + margin-top: 50px; } + +.vote-result { + color: #aaa; + font-size: 18px; } + +/*# sourceMappingURL=style.css.map */ diff --git a/public/stylesheets/style.css.map b/public/stylesheets/style.css.map new file mode 100644 index 0000000..34d267f --- /dev/null +++ b/public/stylesheets/style.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAAA,IAAK;EAEH,IAAI,EAAE,kDAAkD;;AAG1D,CAAE;EACA,KAAK,EAAE,IAAI;EACX,eAAe,EAAE,IAAI;EACrB,OAAQ;IACN,KAAK,EAAE,IAAI;IACX,eAAe,EAAE,IAAI;;AAIzB,QAAS;EACL,OAAO,EAAE,WAAW;EACpB,KAAK,EAAE,IAAI;;AAGf,eAAgB;EACZ,UAAU,EAAE,MAAM;;AAGtB,KAAM;EACF,gBAAgB,EAAE,IAAI;EACtB,OAAO,EAAE,WAAW;EACpB,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,KAAK;EACZ,MAAM,EAAE,KAAK;EACb,UAAU,EAAE,8BAA2B;EACvC,UAAU,EAAE,IAAI;EAChB,WAAQ;IACJ,UAAU,EAAE,+BAA4B;EAE5C,SAAI;IACA,KAAK,EAAE,KAAK;IACZ,MAAM,EAAE,KAAK;EAGjB,cAAS;IACL,UAAU,EAAE,MAAM;IAClB,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,IAAI;IACX,SAAS,EAAE,IAAI;EAGnB,cAAS;IACL,KAAK,EAAE,OAAO;;AAItB,OAAQ;EACJ,OAAO,EAAE,MAAM;;AAGnB,aAAc;EACV,UAAU,EAAE,IAAI;;AAGpB,WAAY;EACR,OAAO,EAAE,QAAQ;;AAGrB,UAAW;EACP,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM;EACd,UAAU,EAAE,MAAM;EAClB,eAAK;IACD,UAAU,EAAE,IAAI;;AAIxB,YAAa;EACT,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,IAAI", +"sources": ["../../styles_sass/style.scss"], +"names": [], +"file": "style.css" +} \ No newline at end of file diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..c321e60 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,30 @@ +var express = require('express'); +var router = express.Router(); + +const sunlightAPI = require('../utils/api') +const Legistlator = require('../models/legislators') + +router.post('/', function(req, res, next) { + if (!(/^\d{5}(?:[-\s]\d{4})?$/.test(req.body.zip))) { + console.log('error out') + res.render('index') + } else { + const zip = req.body.zip + + sunlightAPI.getLegistlatorByZip(zip) + .then((legislators) => { + res.render('legislatorList', {legislators: legislators, zip: zip}); + }) + .catch((err) => { + console.error(err) + res.send(err) + }) + } +}); + +router.get('/', function(req, res, next) { + console.log('error?') + res.render('index', {title: 'Express'}); +}); + +module.exports = router; diff --git a/routes/legislators.js b/routes/legislators.js new file mode 100644 index 0000000..6dd6b7f --- /dev/null +++ b/routes/legislators.js @@ -0,0 +1,31 @@ +var express = require('express'); +var router = express.Router(); + +const sunlightAPI = require('../utils/api') + +/* GET legislator info by id */ +router.get('/:id', function(req, res, next) { + const id = req.params.id + let data = [] + + let legislator = sunlightAPI.getLegislatorByID(id) + let votes = sunlightAPI.getRecentVotesByID(id) + + Promise.all([legislator, votes]) + .then((results) => { + let legislatorObj = { + legislator: results[0], + votes: results[1] + } + return legislatorObj + }) + .then((results) => { + res.render('legislator', {legislator: results.legislator, votes: results.votes}) + }) + .catch((err) => { + console.error(err) + }) + +}); + +module.exports = router; diff --git a/styles_sass/style.scss b/styles_sass/style.scss new file mode 100644 index 0000000..ed6da18 --- /dev/null +++ b/styles_sass/style.scss @@ -0,0 +1,76 @@ +body { +// padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #000; + text-decoration: none; + &:hover { + color: #000; + text-decoration: none; + } +} + +.results { + display: inline-flex; + width: 100%; +} + +.card-container { + text-align: center; +} + +.card { + background-color: #fff; + display: inline-flex; + margin: 50px; + width: 300px; + height: 480px; + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + transition: 0.3s; + &:hover { + box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); + } + img { + width: 300px; + height: 365px; + } + + .chamber { + text-align: center; + margin: 10px; + color: #aaa; + font-size: 16px; + } + + .website { + color: #8aacb8; + } +} + +.single { + display: inline; +} + +.recent-votes { + margin-top: 50px; +} + +.inner-card { + padding: 2px 16px; +} + +.form-card { + max-width: 500px; + margin: 0 auto; + text-align: center; + form { + margin-top: 50px; + } +} + +.vote-result { + color: #aaa; + font-size: 18px; +} \ No newline at end of file diff --git a/utils/api.js b/utils/api.js new file mode 100644 index 0000000..57f8ef8 --- /dev/null +++ b/utils/api.js @@ -0,0 +1,129 @@ +const request = require('request') + +const Legislator = require('../models/legislators') +const Vote = require('../models/votes') + +const URI = 'https://congress.api.sunlightfoundation.com' + +const capitalize = (text) => { + return text.charAt(0).toUpperCase() + text.slice(1) +} + +const formatParty = (party) => { + switch (party) { + case 'R': + return 'Republican' + break + case 'D': + return 'Democrat' + break + case 'I': + return 'Independent' + break + default: + return 'No associated party' + } +} + +const addLegislator = (legislator) => { + return new Legislator( + legislator.bioguide_id, + `https://theunitedstates.io/images/congress/original/${legislator.bioguide_id}.jpg`, + `${legislator.first_name} ${legislator.last_name}`, + capitalize(legislator.chamber), + formatParty(legislator.party), + legislator.phone, + legislator.website + ) +} + +const sunlightAPI = { + getLegistlatorByZip: (zip) => { + return new Promise((resolve, reject) => { + if (!(/^\d{5}(?:[-\s]\d{4})?$/.test(zip))) { + reject('Not a valid zip!') + } + let options = { + url: `${URI}/legislators/locate?zip=${zip}`, + headers: { + 'User-Agent': 'request' + } + } + request(options, (error, response, body) => { + if (!error && response.statusCode == 200) { + let results = JSON.parse(body) + let legislatorList = [] + + // Format the fields + results.results.map((legislator) => { + legislatorList.push( + addLegislator(legislator) + ) + }) + + resolve(legislatorList) + } else { + reject(error) + } + }) + }) + }, + getLegislatorByID: (id) => { + return new Promise((resolve, reject) => { + let options = { + url: `${URI}/legislators?bioguide_id=${id}`, + headers: { + 'User-Agent': 'request' + } + } + + request(options, (error, response, body) => { + if (!error && response.statusCode == 200) { + let results = JSON.parse(body) + + // Format the fields + let legislator = addLegislator(results.results[0]) + + resolve(legislator) + } else { + reject(error) + } + }) + }) + }, + getRecentVotesByID: (id) => { + return new Promise((resolve, reject) => { + let options = { + url: `${URI}/votes?fields=voter_ids,question,bill&voter_ids.${id}__exists=true&bill_id__exists=true`, + headers: { + 'User-Agent': 'request' + } + } + + request(options, (error, response, body) => { + if (!error && response.statusCode == 200) { + let results = JSON.parse(body) + + let voteInfo = [] + + results.results.map((voteData) => { + voteInfo.push( + new Vote( + voteData.bill.bill_id, + voteData.bill.official_title, + voteData.bill.urls.congress, + voteData.question, + voteData.voter_ids[id] + ) + ) + }) + resolve(voteInfo) + } else { + reject(error) + } + }) + }) + } +} + +module.exports = sunlightAPI \ No newline at end of file diff --git a/views/error.hbs b/views/error.hbs new file mode 100644 index 0000000..0659765 --- /dev/null +++ b/views/error.hbs @@ -0,0 +1,3 @@ +

{{message}}

+

{{error.status}}

+
{{error.stack}}
diff --git a/views/index.hbs b/views/index.hbs new file mode 100644 index 0000000..6826c47 --- /dev/null +++ b/views/index.hbs @@ -0,0 +1,13 @@ +
+
+
+

Find-My-Rep

+
+
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/views/layout.hbs b/views/layout.hbs new file mode 100644 index 0000000..a7a8620 --- /dev/null +++ b/views/layout.hbs @@ -0,0 +1,38 @@ + + + + + + + Find-My-Rep + + + + + + {{{body}}} + + + + diff --git a/views/legislator.hbs b/views/legislator.hbs new file mode 100644 index 0000000..717444d --- /dev/null +++ b/views/legislator.hbs @@ -0,0 +1,32 @@ +
+
+
+

{{legislator.chamber}}

+ {{legislator.name}} +
+

{{legislator.name}}

+

{{legislator.party}}

+
+

Phone Number: {{legislator.phone}}

+

Website: {{legislator.website}}

+ +
+
+ +
+

Recent Votes

+
+ {{#each votes as |bill|}} + +
+

{{bill.bill_id}}

+

{{bill.official_title}}

+
+

Vote: {{bill.vote}}

+
+
+
+ {{/each}} + + +
\ No newline at end of file diff --git a/views/legislatorList.hbs b/views/legislatorList.hbs new file mode 100644 index 0000000..2d1bd4f --- /dev/null +++ b/views/legislatorList.hbs @@ -0,0 +1,16 @@ +
+ {{#each legislators as |legislator|}} + + {{/each}} +
\ No newline at end of file