Skip to content

Commit

Permalink
Followed Node training up to #45
Browse files Browse the repository at this point in the history
  • Loading branch information
neilpalima committed Jul 31, 2017
1 parent 18ef6bf commit 83dfa33
Show file tree
Hide file tree
Showing 28 changed files with 11,066 additions and 85 deletions.
4 changes: 3 additions & 1 deletion Node/node-training/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
node_modules/
node_modules/
variables.env
variables-production.env
9 changes: 8 additions & 1 deletion Node/node-training/controllers/authController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const crypto = require('crypto');
const mongoose = require('mongoose');
const User = mongoose.model('User');
const promisify = require('es6-promisify');
const mail = require('../handlers/mail');

exports.login = passport.authenticate('local', {
failureRedirect: '/login',
Expand Down Expand Up @@ -40,7 +41,13 @@ exports.forgot = async (req, res) => {
await user.save();
// 3. Send them an email with the token
const resetURL = `http://${req.headers.host}/account/reset/${user.resetPasswordToken}`;
req.flash('success', `You have been emailed a password reset link. ${resetURL}`);
await mail.send({
user,
filename: 'password-reset',
subject: 'Password Reset',
resetURL
});
req.flash('success', `You have been emailed a password reset link.`);
// 4. redirect to login page
res.redirect('/login');
};
Expand Down
11 changes: 11 additions & 0 deletions Node/node-training/controllers/reviewController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const mongoose = require('mongoose');
const Review = mongoose.model('Review');

exports.addReview = async (req, res) => {
req.body.author = req.user._id;
req.body.store = req.params.id;
const newReview = new Review(req.body);
await newReview.save();
req.flash('success', 'Revivew Saved!');
res.redirect('back');
};
102 changes: 98 additions & 4 deletions Node/node-training/controllers/storeController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const mongoose = require('mongoose');
const Store = mongoose.model('Store');
const User = mongoose.model('User');
const multer = require('multer');
const jimp = require('jimp');
const uuid = require('uuid');
Expand Down Expand Up @@ -43,22 +44,49 @@ exports.resize = async (req, res, next) => {
};

exports.createStore = async (req, res) => {
req.body.author = req.user._id;
const store = await (new Store(req.body)).save();
req.flash('success', `Successfully Created ${store.name}. Care to leave a review?`);
res.redirect(`/store/${store.slug}`);
};

exports.getStores = async (req, res) => {
const page = req.params.page || 1;
const limit = 4;
const skip = (page * limit) - limit;

// 1. Query the database for a list of all stores
const stores = await Store.find();
res.render('stores', { title: 'Stores', stores });
const storesPromise = Store
.find()
.skip(skip)
.limit(limit)
.sort({ created: 'desc' });

const countPromise = Store.count();

const [stores, count] = await Promise.all([storesPromise, countPromise]);
const pages = Math.ceil(count / limit);
if (!stores.length && skip) {
req.flash('info', `Hey! You asked for page ${page}. But that doesn't exist. So I put you on page ${pages}`);
res.redirect(`/stores/page/${pages}`);
return;
}

res.render('stores', { title: 'Stores', stores, page, pages, count });
};

const confirmOwner = (store, user) => {
if (!store.author.equals(user._id)) {
throw Error('You must own a store in order to edit it!');
}
};


exports.editStore = async (req, res) => {
// 1. Find the store given the ID
const store = await Store.findOne({ _id: req.params.id });
// 2. confirm they are the owner of the store
// TODO
confirmOwner(store, req.user);
// 3. Render out the edit form so the user can update their store
res.render('editStore', { title: `Edit ${store.name}`, store });
};
Expand All @@ -77,7 +105,7 @@ exports.updateStore = async (req, res) => {
};

exports.getStoreBySlug = async (req, res, next) => {
const store = await Store.findOne({ slug: req.params.slug });
const store = await Store.findOne({ slug: req.params.slug }).populate('author reviews');
if (!store) return next();
res.render('store', { store, title: store.name });
};
Expand All @@ -93,3 +121,69 @@ exports.getStoresByTag = async (req, res) => {

res.render('tag', { tags, title: 'Tags', tag, stores });
};


exports.searchStores = async (req, res) => {
const stores = await Store
// first find stores that match
.find({
$text: {
$search: req.query.q
}
}, {
score: { $meta: 'textScore' }
})
// the sort them
.sort({
score: { $meta: 'textScore' }
})
// limit to only 5 results
.limit(5);
res.json(stores);
};

exports.mapStores = async (req, res) => {
const coordinates = [req.query.lng, req.query.lat].map(parseFloat);
const q = {
location: {
$near: {
$geometry: {
type: 'Point',
coordinates
},
$maxDistance: 10000 // 10km
}
}
};

const stores = await Store.find(q).select('slug name description location photo').limit(10);
res.json(stores);
};

exports.mapPage = (req, res) => {
res.render('map', { title: 'Map' });
};

exports.heartStore = async (req, res) => {
const hearts = req.user.hearts.map(obj => obj.toString());

const operator = hearts.includes(req.params.id) ? '$pull' : '$addToSet';
const user = await User
.findByIdAndUpdate(req.user._id,
{ [operator]: { hearts: req.params.id } },
{ new: true }
);
res.json(user);
};

exports.getHearts = async (req, res) => {
const stores = await Store.find({
_id: { $in: req.user.hearts }
});
res.render('stores', { title: 'Hearted Stores', stores });
};

exports.getTopStores = async (req, res) => {
const stores = await Store.getTopStores();
res.render('topStores', { stores, title:'⭐ Top Stores!'});
}
35 changes: 35 additions & 0 deletions Node/node-training/handlers/mail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const nodemailer = require('nodemailer');
const pug = require('pug');
const juice = require('juice');
const htmlToText = require('html-to-text');
const promisify = require('es6-promisify');

const transport = nodemailer.createTransport({
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASS
}
});

const generateHTML = (filename, options = {}) => {
const html = pug.renderFile(`${__dirname}/../views/email/${filename}.pug`, options);
const inlined = juice(html);
return inlined;
};

exports.send = async (options) => {
const html = generateHTML(options.filename, options);
const text = htmlToText.fromString(html);

const mailOptions = {
from: `Wes Bos <[email protected]>`,
to: options.user.email,
subject: options.subject,
html,
text
};
const sendMail = promisify(transport.sendMail, transport);
return sendMail(mailOptions);
};
38 changes: 38 additions & 0 deletions Node/node-training/models/Review.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;

const reviewSchema = new mongoose.Schema({
created: {
type: Date,
default: Date.now
},
author: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: 'You must supply an author!'
},
store: {
type: mongoose.Schema.ObjectId,
ref: 'Store',
required: 'You must supply a store!'
},
text: {
type: String,
required: 'Your review must have text!'
},
rating: {
type: Number,
min: 1,
max: 5
}
});

function autopopulate(next) {
this.populate('author');
next();
}

reviewSchema.pre('find', autopopulate);
reviewSchema.pre('findOne', autopopulate);

module.exports = mongoose.model('Review', reviewSchema);
54 changes: 53 additions & 1 deletion Node/node-training/models/Store.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,25 @@ const storeSchema = new mongoose.Schema({
required: 'You must supply an address!'
}
},
photo: String
photo: String,
author: {
type: mongoose.Schema.ObjectId,
ref: 'User',
required: 'You must supply an author'
}
}, {
toJSON: { virtuals: true },
toOjbect: { virtuals: true },
});

// Define our indexes
storeSchema.index({
name: 'text',
description: 'text'
});

storeSchema.index({ location: '2dsphere' });

storeSchema.pre('save', async function(next) {
if (!this.isModified('name')) {
next(); // skip it
Expand All @@ -57,6 +73,42 @@ storeSchema.statics.getTagsList = function() {
{ $group: { _id: '$tags', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]);
};

storeSchema.statics.getTopStores = function() {
return this.aggregate([
// Lookup Stores and populate their reviews
{ $lookup: { from: 'reviews', localField: '_id', foreignField: 'store', as: 'reviews' }},
// filter for only items that have 2 or more reviews
{ $match: { 'reviews.1': { $exists: true } } },
// Add the average reviews field
{ $project: {
photo: '$$ROOT.photo',
name: '$$ROOT.name',
reviews: '$$ROOT.reviews',
slug: '$$ROOT.slug',
averageRating: { $avg: '$reviews.rating' }
} },
// sort it by our new field, highest reviews first
{ $sort: { averageRating: -1 }},
// limit to at most 10
{ $limit: 10 }
]);
}

// find reviews where the stores _id property === reviews store property
storeSchema.virtual('reviews', {
ref: 'Review', // what model to link?
localField: '_id', // which field on the store?
foreignField: 'store' // which field on the review?
});

function autopopulate(next) {
this.populate('reviews');
next();
}

storeSchema.pre('find', autopopulate);
storeSchema.pre('findOne', autopopulate);

module.exports = mongoose.model('Store', storeSchema);
5 changes: 4 additions & 1 deletion Node/node-training/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ const userSchema = new Schema({
trim: true
},
resetPasswordToken: String,
resetPasswordExpires: Date
resetPasswordExpires: Date,
hearts: [
{ type: mongoose.Schema.ObjectId, ref: 'Store' }
]
});

userSchema.virtual('gravatar').get(function() {
Expand Down
Loading

0 comments on commit 83dfa33

Please sign in to comment.