History: Photo sharing

Revision made 8 years ago by Francisco Presencia. Go to the last revision.

We are going to create a photo sharing service where the user is able to upload pictures and like them. You can see the finished code in Github.

We will be using a simple MVC, so our file structure will be:

/controllers
  /photos.js
/models
  /photos.js
/views
  /index.pug
  /layout.pug
/public
  /images
    /1.jpg
    /2.jpg
    ...
  /style.css
/.env
/.gitignore
/app.js
/package.json
/Procfile
/routes.js

So far there will be no user auth as it'd take too long to explain it all. We will need to install few packages within our web folder, which will be explained as we go:

npm install auto-load cloudinary dotenv express formidable mongoose pug --save

A small reminder about requests flow; when the user requests any page in any way, it will reach the app.js. In that file we will have all of our server initial setup, and then the flow will continue with our router. From the router we specify what call goes where.

So we first set up our app.js:

// Load the environment configuration from ".env"
require('dotenv').config();

// Import the modules that we will use in this file
var express = require('express');
var mongoose = require('mongoose');
var routes = require('./routes');

// Server configuration
var app = express();
app.set('view engine', 'pug');  // The "html engine"
app.use(express.static('public'));  // Where the static files are

// Connect to the database before accepting any request
mongoose.connect('mongodb://localhost/test');
var db = mongoose.connection;
db.once('open', function(){

  // Use the routes stored in ./routes.js
  app.use(routes);
 
  // Actually start listening to the requests
  app.listen(process.env.PORT || 3000);
});

Then we set up the basic routes. We will be handling requesting the main page with all of the images and uploading a new image with some extra info. In routes.js:

var express = require('express');
var router = express.Router();
var controllers = require('auto-load')('controllers');

router.get('/', controllers.photos.index);

router.post('/photos', controllers.photos.upload);

module.exports = router;

Finally, we will write the main logic for each of our calls in controllers/photos.js. If we had more logic, then we would create different controllers and routes to split it all properly.

// Mock up the data for the index
module.exports.index = function(req, res){
  res.render('index', {
    photos: [
      '/images/1.jpg',
      '/images/2.jpg',
      '/images/3.jpg',
      '/images/4.jpg',
      '/images/5.jpg'
    ]
  });
}

// Redirect to home after POSTing to /photos
module.exports.upload = function(req, res, next){
  res.redirect('/');
}

Our layout.pug:

doctype html
html
  head
    title= title
    link(rel="stylesheet" href="https://cdn.jsdelivr.net/picnicss/6.0.0/plugins.min.css")
    link(rel='stylesheet', href='style.css')
    block head
  body
    block content

    script.
      document.addEventListener("DOMContentLoaded", function() {
        [].forEach.call(document.querySelectorAll('.dropimage'), function(img){
          img.onchange = function(e){
            var inputfile = this, reader = new FileReader();
            reader.onloadend = function(){
              inputfile.style['background-image'] = 'url('+reader.result+')';
            }
            reader.readAsDataURL(e.target.files[0]);
          }
        });
      });

In index.pug:

extends layout

block content

  h1= name

  div.flex.one.two-500.five-900
    each photo in photos
      div.photo
        img(src=photo)
        button.like Like

In our style.css:

body {
  width: 100%;
  max-width: 1000px;
  margin: 0 auto;
  padding: 20px;
}

img {
  width: 100%;
}

So far it displays a list with the images decently formatted. You can add any more styles and parts of the website that you see fit.

Uploading the pictures

To upload the pictures we need to add some logic to handle it. Heroku and other Node.js hosting platforms don't have a filesystem where you can store the user-uploaded pictures. So we will use Cloudinary to store our images and try to leave this logic in only one place so we can switch vendors easily if we want to.

But first things first; let's add a form to our website. We add this to the end of our index.pug:

form(action="/photos" method="POST" enctype="multipart/form-data")
  div.flex.two
    div
      label.dropimage
        input(type="file" name="image" title="Drop image or click me")
    div
      label Title:
        input(name="title" placeholder="Title")
      label Nickname:
        input(name="nick" placeholder="Nickname")
      input(type="submit" value="Upload")

The important bits are:

  • Remember to use the method=POST
  • The enctype="multipart/form-data" to say that there will be files posted
  • Use an <input type="file"> to upload the pictures.

Now let's create an account in Cloudinary and put our values in .env :

cloud=xxx
key=yyy
secret=zzz

Our controllers/photos.js then uses that configuration with the official library to upload the files. We will also use formidable to handle the posting of the images in the form:

var models = require('auto-load')('models'); // so far empty
var formidable = require('formidable');
var cloudinary = require('cloudinary');

// Use our configuration in .env
cloudinary.config({
  cloud_name: process.env.cloud,
  api_key: process.env.key,
  api_secret: process.env.secret
});

module.exports.index = function(req, res){
  models.photos.find({}, function(err, images){
    res.render('index', { photos: images });
  });
}

module.exports.upload = function(req, res, next){
  // Handle the form with an image
  var form = new formidable.IncomingForm();
  form.parse(req, function (err, fields, files) {
    if (err) next(err);

    // Actually upload it to cloudinary
    cloudinary.uploader.upload(files.image.path, function (result) {

      // Let's force https in the image:
      var url = result.url.replace('http://', 'https://');

      // Go back home
      res.redirect('/');
    });
  });
}

Now the files are stored in our Cloudinary account and we can see them there; but we will have to store the reference in the database so we can show them there.

Store in the database

We will use mongoose, which is a modeling tool that makes MongoDB somewhat easier. Let's create our model to store the photo references and some extra data models/photos.js:

var mongoose = require('mongoose');

var photosSchema = mongoose.Schema({
  title: { type: String, required: true },
  nickname: { type: String, unique: true, required: true },
  // Do we want other metadata as image location? :3
  url: { type: String, required: true },
  likes: [{ type: String }]
});

module.exports = mongoose.model('Photos', photosSchema);

We save the image reference in the database after we upload it to cloudinary. To save an item in the database we have to create it with new applied to the model we just created and then apply the method .save():

var Photo = new models.photos({
  url: url,
  title: fields.title,
  nickname: fields.nick,
});
Photo.save(redirect);

We also want to query all of the images for the home page, so we will be using the database for that. In here, we retrieve all of the files:

// Empty query {} means it will just find them all
models.photos.find({}, function(err, photos){
  if (err) // handle the error
  // here 'photos' has all the pictures
});

So let's put that together into our controllers/photos.js:

var models = require('auto-load')('models');
var formidable = require('formidable');
var cloudinary = require('cloudinary');

cloudinary.config({
  cloud_name: process.env.cloud,
  api_key: process.env.key,
  api_secret: process.env.secret
});

module.exports.index = function(req, res){
  models.photos.find({}, function(err, images){
    res.render('index', { photos: images });
  });
}

module.exports.upload = function(req, res, next){
  var form = new formidable.IncomingForm();

  function redirect(err){
    if (err) next(err);
    res.redirect('/');
  }

  function parseForm(err, fields, files) {
    if (err) next(err);
    cloudinary.uploader.upload(files.image.path, function (result) {
      // Let's force https in the image:
      var url = result.url.replace('http://', 'https://');

      var Photo = new models.photos({
        url: url,
        title: fields.title,
        nickname: fields.nick,
      });
      Photo.save(redirect);
    });
  }

  form.parse(req, parseForm);
}

Like and unlike

First we will split our scripts into a separate javascript.js so it stays small. In layout.pug:

doctype html
html
  head
    title= title
    link(rel="stylesheet" href="https://cdn.jsdelivr.net/picnicss/6.0.0/plugins.min.css")
    link(rel='stylesheet', href='style.css')
    block head
  body
    block content

    script(src="https://code.jquery.com/jquery-3.1.1.min.js")
    script(src="javascript.js")

In javascript.js:

document.addEventListener("DOMContentLoaded", function() {
  [].forEach.call(document.querySelectorAll('.dropimage'), function(img){
    img.onchange = function(e){
      var inputfile = this, reader = new FileReader();
      reader.onloadend = function(){
        inputfile.style['background-image'] = 'url('+reader.result+')';
      }
      reader.readAsDataURL(e.target.files[0]);
    }
  });
});

$('.like').on('click', function(e){
  var liked = $(e.target).hasClass('liked');
  var id = $(e.target).attr('id');
  $.post('/likes/' + id, { liked: !liked }, function(){
    var text = liked ? 'Like' : 'Unlike';
    $(e.target).toggleClass('liked').text(text);
  });
});