├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── Vagrantfile ├── app ├── controllers │ └── contacts.js ├── models │ └── contact.js └── views │ ├── contacts │ ├── edit.jade │ ├── form.jade │ ├── index.jade │ └── new.jade │ └── layouts │ └── default.jade ├── config ├── routes.js └── sockets.js ├── index.js ├── package.json └── static ├── css └── contacts.css └── js └── contacts.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vagrant 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Tuts+ 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bash -c 'source /home/vagrant/.nvm/nvm.sh && exec nodemon index.js' 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Easy Node.js Development Environment With Vagrant][published url] 2 | ## Instructor: [Markus Mühlberger][instructor url] 3 | 4 | 5 | When developing a Node.js app, you will probably need one or more of these dependencies: a database server, a key-value store, a background worker or a search engine. Installing and running these on your local machine gets messy fast. Data from your app can also interfere with other apps you are developing on the same system. 6 | 7 | With Vagrant you can isolate these dependencies in a virtual machine, start and stop them all together, and save yourself having to setup everything all over when your system crashes or when you move to a new computer. In this course, you will learn how to create a virtual machine with Vagrant, configure it for Node.js development and share it with others. 8 | 9 | 10 | ## Source Files Description 11 | 12 | The source files contain the Node.js app we are going to use as an example application in this course. It uses MongoDB and Redis for data and session storage, as well as socket.io for real-time communication with the client. Of course, there is also a `Vagrantfile` for which defines Vagrant box! 13 | 14 | ------ 15 | 16 | These are source files for the Tuts+ course: [Easy Node.js Development Environment With Vagrant][published url] 17 | 18 | Available on [Tuts+](https://tutsplus.com). Teaching skills to millions worldwide. 19 | 20 | [published url]: https://code.tutsplus.com/courses/easy-nodejs-development-environment-with-vagrant 21 | [instructor url]: https://tutsplus.com/authors/markus-muehlberger 22 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure(2) do |config| 9 | # The most common configuration options are documented and commented below. 10 | # For a complete reference, please see the online documentation at 11 | # https://docs.vagrantup.com. 12 | 13 | # Every Vagrant development environment requires a box. You can search for 14 | # boxes at https://atlas.hashicorp.com/search. 15 | # This line has been updated to point to the "bento/ubuntu-14.04" box instead of "chef/ubuntu-14.04" 16 | config.vm.box = "bento/ubuntu-14.04" 17 | 18 | # Disable automatic box update checking. If you disable this, then 19 | # boxes will only be checked for updates when the user runs 20 | # `vagrant box outdated`. This is not recommended. 21 | # config.vm.box_check_update = false 22 | 23 | # Create a forwarded port mapping which allows access to a specific port 24 | # within the machine from a port on the host machine. In the example below, 25 | # accessing "localhost:8080" will access port 80 on the guest machine. 26 | config.vm.network "forwarded_port", guest: 4000, host: 4000 27 | 28 | # Create a private network, which allows host-only access to the machine 29 | # using a specific IP. 30 | # config.vm.network "private_network", ip: "192.168.33.10" 31 | 32 | # Create a public network, which generally matched to bridged network. 33 | # Bridged networks make the machine appear as another physical device on 34 | # your network. 35 | # config.vm.network "public_network" 36 | 37 | # Share an additional folder to the guest VM. The first argument is 38 | # the path on the host to the actual folder. The second argument is 39 | # the path on the guest to mount the folder. And the optional third 40 | # argument is a set of non-required options. 41 | # config.vm.synced_folder "../data", "/vagrant_data" 42 | 43 | # Provider-specific configuration so you can fine-tune various 44 | # backing providers for Vagrant. These expose provider-specific options. 45 | # Example for VirtualBox: 46 | # 47 | # config.vm.provider "virtualbox" do |vb| 48 | # # Display the VirtualBox GUI when booting the machine 49 | # vb.gui = true 50 | # 51 | # # Customize the amount of memory on the VM: 52 | # vb.memory = "1024" 53 | # end 54 | # 55 | # View the documentation for the provider you are using for more 56 | # information on available options. 57 | 58 | # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies 59 | # such as FTP and Heroku are also available. See the documentation at 60 | # https://docs.vagrantup.com/v2/push/atlas.html for more information. 61 | # config.push.define "atlas" do |push| 62 | # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" 63 | # end 64 | 65 | # Enable provisioning with a shell script. Additional provisioners such as 66 | # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the 67 | # documentation for more information about their specific syntax and use. 68 | config.vm.provision "shell", privileged: false, inline: <<-SHELL 69 | sudo apt-get update 70 | sudo apt-get install -y build-essential curl 71 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.25.4/install.sh | bash 72 | source ~/.nvm/nvm.sh 73 | nvm install node 74 | nvm alias default node 75 | 76 | sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 77 | echo "deb http://repo.mongodb.org/apt/ubuntu "$(lsb_release -sc)"/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.0.list 78 | sudo apt-get update 79 | sudo apt-get install -y mongodb-org 80 | 81 | curl http://download.redis.io/redis-stable.tar.gz -o redis-stable.tar.gz 82 | tar xzf redis-stable.tar.gz 83 | cd redis-stable 84 | cd deps 85 | make hiredis lua jemalloc linenoise 86 | cd .. 87 | make 88 | sudo make install 89 | sudo mkdir -p /var/redis/6379 /etc/redis 90 | sudo cp utils/redis_init_script /etc/init.d/redis_6379 91 | sudo update-rc.d redis_6379 defaults 92 | sudo cp redis.conf /etc/redis/6379.conf 93 | sudo sed -i 's/daemonize no/daemonize yes/g' /etc/redis/6379.conf 94 | sudo sed -i 's/redis.pid/redis_6379.pid/g' /etc/redis/6379.conf 95 | sudo sed -i 's/# bind 127.0.0.1/bind 127.0.0.1/g' /etc/redis/6379.conf 96 | sudo sed -i 's/logfile ""/logfile \/var\/log\/redis_6379.log/g' /etc/redis/6379.conf 97 | sudo sed -i 's/dir .\//dir \/var\/redis\/6379/g' /etc/redis/6379.conf 98 | sudo service redis_6379 start 99 | 100 | cd /vagrant 101 | npm install 102 | npm install -g nodemon 103 | 104 | sudo apt-get install -y ruby 105 | sudo gem install foreman 106 | sudo foreman export upstart /etc/init -a nodejs -u vagrant -p 4000 107 | sudo service nodejs start 108 | SHELL 109 | end 110 | -------------------------------------------------------------------------------- /app/controllers/contacts.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Contact = mongoose.model('Contact'); 3 | var extend = require('util')._extend; 4 | 5 | exports.load = function (req, res, next, id) { 6 | Contact.findOne({ _id : id }, function (err, contact) { 7 | if (err) return next(err); 8 | if (!contact) return next(new Error('Not Found')); 9 | req.contact = contact; 10 | next(); 11 | }); 12 | }; 13 | 14 | exports.index = function (req, res) { 15 | Contact.find(function (err, contacts) { 16 | if (err) res.render('500'); 17 | res.render('contacts/index', { 18 | contacts: contacts 19 | }); 20 | }); 21 | }; 22 | 23 | exports.new = function (req, res) { 24 | res.render('contacts/new', { 25 | contact: new Contact() 26 | }); 27 | }; 28 | 29 | exports.create = function (req, res) { 30 | var contact = new Contact(req.body); 31 | contact.validate(function (err) { 32 | if (err) res.render('500'); 33 | contact.save(function (err) { 34 | if (!err) { 35 | req.flash('success', 'Your contact has been created.'); 36 | return res.redirect('/contacts'); 37 | } 38 | res.render('contacts/new', { 39 | contact: contact, 40 | errors: err.errors || err 41 | }); 42 | }); 43 | }); 44 | }; 45 | 46 | exports.edit = function (req, res) { 47 | res.render('contacts/edit', { 48 | contact: req.contact 49 | }); 50 | }; 51 | 52 | exports.update = function (req, res) { 53 | var contact = req.contact; 54 | extend(contact, req.body); 55 | contact.validate(function (err) { 56 | if (err) res.render('500'); 57 | contact.save(function (err) { 58 | if (!err) { 59 | req.flash('success', 'Your contact has been updated.'); 60 | return res.redirect('/contacts'); 61 | } 62 | res.render('contacts/edit', { 63 | contact: contact, 64 | errors: err.errors || err 65 | }); 66 | }); 67 | }); 68 | }; 69 | 70 | exports.destroy = function (req, res) { 71 | req.contact.remove(function (err) { 72 | req.flash('success', 'Your contact has been deleted.'); 73 | res.redirect('/contacts'); 74 | }) 75 | }; 76 | -------------------------------------------------------------------------------- /app/models/contact.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var faker = require('faker'); 3 | var mongoose = require('mongoose'); 4 | 5 | var schema = mongoose.Schema({ 6 | firstName: String, 7 | lastName: String, 8 | streetAddress: String, 9 | zipCode: String, 10 | city: String, 11 | countryCode: String, 12 | email: String, 13 | phoneNumber: String, 14 | twitter: String, 15 | avatarUrl: String 16 | }); 17 | 18 | schema.statics = { 19 | generateSamples: function (sampleSize, callback) { 20 | var self = this; 21 | 22 | async.times(sampleSize, function (n, next) { 23 | var userName = faker.helpers.slugify(faker.internet.userName()).replace(/\./g, '').substring(0, 15); 24 | 25 | new self({ 26 | firstName: faker.name.firstName(), 27 | lastName: faker.name.lastName(), 28 | streetAddress: faker.address.streetAddress(), 29 | zipCode: faker.address.zipCode(), 30 | city: faker.address.city(), 31 | countryCode: faker.address.countryCode(), 32 | email: faker.internet.email(), 33 | phoneNumber: faker.phone.phoneNumber(), 34 | twitter: userName, 35 | avatarUrl: "https://robohash.org/" + userName + "?size=80x80" 36 | }).save(function (err, contact) { 37 | next(err, contact); 38 | }); 39 | }, callback); 40 | } 41 | }; 42 | 43 | module.exports = mongoose.model('Contact', schema); 44 | -------------------------------------------------------------------------------- /app/views/contacts/edit.jade: -------------------------------------------------------------------------------- 1 | extends form.jade 2 | -------------------------------------------------------------------------------- /app/views/contacts/form.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default.jade 2 | 3 | block content 4 | if contact.isNew 5 | - var action = "/contacts" 6 | else 7 | - var action = "/contacts/" + contact.id 8 | 9 | form(method="post" action=action) 10 | input(type="hidden" name="_csrf" value=csrf_token) 11 | 12 | if !contact.isNew 13 | input(type="hidden" name="_method" value="PUT") 14 | 15 | .row 16 | .form-group.col-sm-6 17 | label.control-label(for="firstName") First Name 18 | input.form-control(type="text" name="firstName" value=contact.firstName) 19 | .form-group.col-sm-6 20 | label.control-label(for="lastName") Last Name 21 | input.form-control(type="text" name="lastName" value=contact.lastName) 22 | 23 | .form-group 24 | label.control-label(for="streetAddress") Street Address 25 | input.form-control(type="text" name="streetAddress" value=contact.streetAddress) 26 | 27 | .row 28 | .form-group.col-sm-2 29 | label.control-label(for="countryCode") Country Code 30 | input.form-control(type="text" name="countryCode" value=contact.countryCode) 31 | .form-group.col-sm-2 32 | label.control-label(for="zipCode") Zip Code 33 | input.form-control(type="text" name="zipCode" value=contact.zipCode) 34 | .form-group.col-sm-8 35 | label.control-label(for="city") City 36 | input.form-control(type="text" name="city" value=contact.city) 37 | 38 | .row 39 | .form-group.col-sm-4 40 | label.control-label(for="phoneNumber") Phone Number 41 | input.form-control(type="text" name="phoneNumber" value=contact.phoneNumber) 42 | .form-group.col-sm-4 43 | label.control-label(for="email") Email 44 | input.form-control(type="text" name="email" value=contact.email) 45 | .form-group.col-sm-4 46 | label.control-label(for="twitter") Twitter 47 | input.form-control(type="text" name="twitter" value=contact.twitter) 48 | 49 | .form-group 50 | label.control-label(for="avatarUrl") Avatar URL 51 | input.form-control(type="text" name="avatarUrl" value=contact.avatarUrl) 52 | 53 | .actions 54 | button.btn.btn-success(type="submit") 55 | if contact.isNew 56 | | Create Contact 57 | else 58 | | Save Contact 59 | |   60 | a.btn.btn-default(href="/contacts") Back to Contacts 61 | -------------------------------------------------------------------------------- /app/views/contacts/index.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default.jade 2 | 3 | block content 4 | .pull-right 5 | a.btn.btn-success(href="/contacts/new") New Contact 6 | div 7 | button#generate-samples.btn.btn-info Generate Sample Contacts 8 | 9 | .contacts.row 10 | .sample-contact.hidden 11 | .contact 12 | .pull-left 13 | img.avatar 14 | a.btn.btn-xs.btn-warning.btn-block.edit-link Edit 15 | form(method="POST") 16 | input(type="hidden" name="_method" value="DELETE") 17 | button.btn.btn-xs.btn-danger.btn-block.delete-link(type="submit") Delete 18 | p.name #[span.firstName] #[span.lastName] 19 | p 20 | a.twitter-link @#[span.twitter] 21 | p. 22 | #[span.streetAddress]
23 | #[span.countryCode] #[span.zipCode] #[span.city] 24 | p 25 | | Phone: #[span.phone]
26 | span.email-wrap Email: #[a.email] 27 | if contacts.length 28 | for contact in contacts 29 | .col-sm-4 30 | .contact 31 | .pull-left 32 | img.avatar(src=contact.avatarUrl) 33 | a.btn.btn-xs.btn-warning.btn-block.edit-link(href="/contacts/" + contact.id + "/edit") Edit 34 | form(method="POST" action="/contacts/" + contact.id) 35 | input(type="hidden" name="_method" value="DELETE") 36 | button.btn.btn-xs.btn-danger.btn-block.delete-link(type="submit") Delete 37 | p.name #{contact.firstName} #{contact.lastName} 38 | p 39 | a(href="https://twitter.com/" + contact.twitter) @#{contact.twitter} 40 | p. 41 | #{contact.streetAddress}
42 | #{contact.countryCode} #{contact.zipCode} #{contact.city} 43 | p 44 | | Phone: #{contact.phoneNumber}
45 | span.email-wrap Email: #[a.email(href="mailto:" + contact.email)= contact.email] 46 | else 47 | .col-sm-12.text-center. 48 | There are no contacts yet. #[a(href="/contacts/new") Create one]. 49 | -------------------------------------------------------------------------------- /app/views/contacts/new.jade: -------------------------------------------------------------------------------- 1 | extends form.jade 2 | -------------------------------------------------------------------------------- /app/views/layouts/default.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title Contacts 5 | link(rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css") 6 | link(rel="stylesheet" href="/css/contacts.css") 7 | body 8 | .container 9 | h1 Contacts 10 | hr 11 | 12 | .notifications 13 | if success && success.length 14 | for message in success 15 | .fade.in.alert.alert-success 16 | button.close(type="button" data-dismiss="alert") × 17 | message 18 | 19 | block content 20 | 21 | hr 22 | p.text-center 23 | | Robots lovingly delivered by 24 | a(href="http://robohash.org" target="_blank") Robohash.org 25 | | . 26 | 27 | script(src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js") 28 | script(src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js") 29 | script(src="/socket.io/socket.io.js") 30 | script(src="/js/contacts.js") 31 | -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | var contacts = require('../app/controllers/contacts'); 2 | 3 | module.exports = function (app) { 4 | app.param('id', contacts.load); 5 | app.get('/contacts', contacts.index); 6 | app.get('/contacts/new', contacts.new); 7 | app.post('/contacts', contacts.create); 8 | app.get('/contacts/:id/edit', contacts.edit); 9 | app.put('/contacts/:id', contacts.update); 10 | app.delete('/contacts/:id', contacts.destroy); 11 | 12 | app.get('/', contacts.index); 13 | } 14 | -------------------------------------------------------------------------------- /config/sockets.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Contact = mongoose.model('Contact'); 3 | 4 | module.exports = function (server) { 5 | var io = require('socket.io').listen(server); 6 | 7 | io.sockets.on('connection', function (socket) { 8 | socket.on('generateContacts', function () { 9 | var contact = Contact.generateSamples(5, function (err, contacts) { 10 | setTimeout(function () { 11 | if (err) { 12 | socket.emit('notification', { 13 | type: 'danger', 14 | message: err.message, 15 | }); 16 | } else { 17 | socket.emit('notification', { 18 | type: 'success', 19 | message: 'Your robot contacts have been created', 20 | }); 21 | socket.emit('contacts', contacts); 22 | } 23 | }, 3000); 24 | }); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var bodyParser = require('body-parser'); 2 | var express = require('express'); 3 | var flash = require('connect-flash'); 4 | var http = require('http'); 5 | var logger = require('morgan'); 6 | var methodOverride = require('method-override'); 7 | var mongoose = require('mongoose'); 8 | var path = require('path'); 9 | var session = require('express-session'); 10 | 11 | var app = express(); 12 | var SessionStore = require('connect-redis')(session); 13 | 14 | mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/contacts'); 15 | 16 | // Bootstrap models 17 | var models = { 18 | Contact: require('./app/models/contact') 19 | } 20 | 21 | app.set('port', process.env.PORT || 4000); 22 | 23 | app.set('view engine', 'jade'); 24 | app.set('views', __dirname + '/app/views'); 25 | app.set('view options', { layout: 'layouts/default' }); 26 | 27 | app.use(logger('dev')); 28 | 29 | app.use(session({ 30 | secret: 'shhhhhhhhh!', 31 | saveUninitialized: false, 32 | resave: true, 33 | store: new SessionStore({ 34 | cookie: { }, 35 | db: 1, 36 | host: 'localhost' 37 | }) 38 | })); 39 | app.use(flash()); 40 | 41 | app.use(express.static(path.join(__dirname, 'static'))); 42 | 43 | app.use(bodyParser.urlencoded({ extended: false })); 44 | app.use(methodOverride(function (req, res) { 45 | if (req.body && typeof req.body === 'object' && '_method' in req.body) { 46 | var method = req.body._method; 47 | delete req.body._method; 48 | return method; 49 | } 50 | })); 51 | 52 | require('./config/routes')(app); 53 | 54 | var server = http.createServer(app); 55 | 56 | require('./config/sockets')(server); 57 | 58 | server.listen(app.get('port'), function () { 59 | console.log('Express server running on http://localhost:' + app.get('port')); 60 | }); 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "robot-contacts", 3 | "version": "1.0.0", 4 | "description": "A contacts application for your robotic friends", 5 | "main": "index.js", 6 | "repository": "https://github.com/tutsplus/easy-nodejs-development-environment-with-vagrant.git", 7 | "private": true, 8 | "dependencies": { 9 | "async": "^1.4.0", 10 | "body-parser": "^1.13.2", 11 | "connect-flash": "^0.1.1", 12 | "connect-redis": "^2.4.1", 13 | "express": "^4.13.1", 14 | "express-session": "^1.11.3", 15 | "faker": "^3.0.0", 16 | "jade": "^1.11.0", 17 | "method-override": "^2.3.4", 18 | "mongoose": "^4.1.0", 19 | "morgan": "^1.6.1", 20 | "socket.io": "^1.3.6" 21 | }, 22 | "devDependencies": {}, 23 | "scripts": { 24 | "test": "echo \"Error: no test specified\" && exit 1" 25 | }, 26 | "author": "Markus Mühlberger " 27 | } 28 | -------------------------------------------------------------------------------- /static/css/contacts.css: -------------------------------------------------------------------------------- 1 | .contact { 2 | margin: 15px 0; 3 | padding: 10px; 4 | border: 1px solid #DDD; 5 | background: #F0F0F0; 6 | } 7 | 8 | .contact .avatar { 9 | max-width: 80px; 10 | border: 1px solid #CCC; 11 | background: white; 12 | } 13 | 14 | .contact a.edit-link, .contact a.delete-link { 15 | text-align: center; 16 | margin: 10px 0; 17 | } 18 | 19 | .contact .name { 20 | font-size: 18px; 21 | font-weight: bold; 22 | } 23 | 24 | .contact .email-wrap { 25 | display: inline-block; 26 | width: 100%; 27 | white-space: nowrap; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | } 31 | 32 | .contact p { 33 | margin-left: 90px; 34 | } 35 | 36 | .contact:first-of-type, .contact:last-child { margin-bottom: 0; } 37 | -------------------------------------------------------------------------------- /static/js/contacts.js: -------------------------------------------------------------------------------- 1 | function createNotification(type, message) { 2 | var alert = $('
') 3 | .addClass('fade in alert alert-dismissable alert-' + type) 4 | .html('' + message); 5 | $('.notifications').append(alert); 6 | window.setTimeout(function() { 7 | alert.alert('close'); 8 | }, 1500); 9 | } 10 | 11 | $(document).on('ready', function () { 12 | var socket = io.connect(document.origin); 13 | socket.on('notification', function(data) { 14 | createNotification(data.type, data.message); 15 | }); 16 | 17 | socket.on('contacts', function (contacts) { 18 | contacts.forEach(function (contact) { 19 | var element = $('.sample-contact').clone(); 20 | element.removeClass('sample-contact hidden').addClass('col-sm-4'); 21 | element.find('.avatar').attr('src', contact.avatarUrl); 22 | element.find('.edit-link').attr('href', "/contacts/" + contact._id + "/edit"); 23 | element.find('form').attr('action', "/contacts/" + contact._id); 24 | element.find('.firstName').text(contact.firstName); 25 | element.find('.lastName').text(contact.lastName); 26 | element.find('.twitter-link').attr('href', "https://twitter.com/" + contact.twitter); 27 | element.find('.twitter').text(contact.twitter); 28 | element.find('.streetAddress').text(contact.streetAddress); 29 | element.find('.countryCode').text(contact.countryCode); 30 | element.find('.zipCode').text(contact.zipCode); 31 | element.find('.city').text(contact.city); 32 | element.find('.phone').text(contact.city); 33 | element.find('.email').text(contact.email); 34 | 35 | $('.contacts').prepend(element); 36 | }); 37 | 38 | $('.contacts .col-sm-12').remove(); 39 | }); 40 | 41 | $('#generate-samples').on('click', function (e) { 42 | e.preventDefault(); 43 | 44 | createNotification('info', 'Your request has been dispatched'); 45 | 46 | socket.emit('generateContacts'); 47 | }) 48 | }); 49 | --------------------------------------------------------------------------------