diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ec544c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.env \ No newline at end of file diff --git a/.sequelizerc b/.sequelizerc new file mode 100644 index 0000000..996843c --- /dev/null +++ b/.sequelizerc @@ -0,0 +1,17 @@ +var path = require("path"); + +// Set options +var config = { + config: "./config/sequelize.js", + "migrations-path": "./migrations/sequelize", + "seeders-path": "./seeds/sequelize", + "models-path": "./models/sequelize" +}; + +// Resolve paths to absolute paths +Object.keys(config).forEach(key => { + config[key] = path.resolve(config[key]); +}); + +// Export like any normal module +module.exports = config; diff --git a/README.md b/README.md index 47801d7..eb513db 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ +Greg and Stephanie + # project_mimirs_market A Viking eCommerce store for Thunder Gods that like to buy "Antique Wooden Pizzas" diff --git a/app.js b/app.js new file mode 100644 index 0000000..3293489 --- /dev/null +++ b/app.js @@ -0,0 +1,59 @@ +const express = require("express"); +const app = express(); +const exphbs = require("express-handlebars"); +const bodyParser = require("body-parser"); +const methodOverride = require("method-override"); +const getPostSupport = require("express-method-override-get-post-support"); +const session = require("express-session"); +const mongooseModels = require("./models/mongoose"); +const sqlModels = require("./models/sequelize"); +const ProductsController = require("./controllers/products"); +const mongoose = require("mongoose"); + +const products = require("./routes/products"); +const cart = require("./routes/cart"); +const checkout = require("./routes/checkout"); +const admin = require("./routes/admin"); + +if (process.env.NODE_ENV !== "production") { + require("dotenv").config(); +} + +app.use(bodyParser.urlencoded({ extended: true })); +app.use(methodOverride(getPostSupport.callback, getPostSupport.options)); + +app.use(express.static(__dirname + "/public")); + +const hbs = exphbs.create({ + partialsDir: "views/partials", + defaultLayout: "main" +}); +app.engine("handlebars", hbs.engine); +app.set("view engine", "handlebars"); + +app.use( + session({ + secret: "123456", + resave: false, + saveUninitialized: true + }) +); + +app.use((req, res, next) => { + if (mongoose.connection.readyState) { + next(); + } else { + require("./mongo")().then(() => next()); + } +}); + +app.use("/products", products); +app.use("/cart", cart); +app.use("/checkout", checkout); +app.use("/admin", admin); + +app.get("/", ProductsController.listProducts); + +app.listen(3000, () => { + console.log("Now listening..."); +}); diff --git a/config/mongo.json b/config/mongo.json new file mode 100644 index 0000000..45db7ea --- /dev/null +++ b/config/mongo.json @@ -0,0 +1,13 @@ +{ + "development": { + "database": "project_mimirs_market_development", + "host": "localhost" + }, + "test": { + "database": "project_mimirs_market_test", + "host": "localhost" + }, + "production": { + "use_env_variable": "MONGO_URL" + } +} diff --git a/config/sequelize.js b/config/sequelize.js new file mode 100644 index 0000000..fc5190b --- /dev/null +++ b/config/sequelize.js @@ -0,0 +1,25 @@ +require("dotenv").config(); + +module.exports = { + development: { + username: process.env.USERNAME, + password: process.env.PASSWORD, + database: "mimirs_market_development", + host: "127.0.0.1", + dialect: "postgres" + }, + test: { + username: "root", + password: null, + database: "database_test", + host: "127.0.0.1", + dialect: "mysql" + }, + production: { + username: "root", + password: null, + database: "database_production", + host: "127.0.0.1", + dialect: "mysql" + } +}; diff --git a/controllers/products.js b/controllers/products.js new file mode 100644 index 0000000..b0dbbf3 --- /dev/null +++ b/controllers/products.js @@ -0,0 +1,149 @@ +const models = require("./../models/sequelize"); + +module.exports = { + listProducts: (req, res) => { + let productIds = []; + + if (req.session.cart) { + productIds = req.session.cart.map(item => { + return Number(item.id); + }); + } + + let params = { + include: [ + { + model: models.Category + } + ], + order: [["name", "ASC"]] + }; + + if (Object.keys(req.query).length) { + params = parseParams(params, req.query); + } + + models.Product.findAll(params).then(products => { + products.forEach(product => { + if (productIds.indexOf(product.id) > -1) { + product.inCart = true; + } + }); + + models.Category.findAll({ order: ["id"] }).then(categories => { + res.render("index", { products, categories }); + }); + }); + }, + + singleProduct: (req, res) => { + const id = req.params.id; + + let productIds = []; + + if (req.session.cart) { + productIds = req.session.cart.map(item => { + return Number(item.id); + }); + } + + models.Product + .findById(id, { + include: [ + { + model: models.Category + } + ] + }) + .then(product => { + if (productIds.indexOf(Number(id)) > -1) { + product.inCart = true; + } + models.Product + .findAll({ + where: { categoryId: product.categoryId, id: { $ne: product.id } }, + limit: 6, + include: [ + { + model: models.Category + } + ] + }) + .then(relatedProducts => { + relatedProducts.forEach(relatedProduct => { + if (productIds.indexOf(relatedProduct.id) > -1) { + relatedProduct.inCart = true; + } + }); + res.render("product", { product, relatedProducts }); + }); + }); + }, + + showCart: (req, res) => { + let quantities = {}; + let productIds = []; + + if (req.session.cart) { + productIds = req.session.cart.map(item => { + quantities[item.id] = item.quantity; + return item.id; + }); + } + + let p = new Promise((resolve, reject) => { + models.Product + .findAll({ + include: [ + { + model: models.Category + } + ], + where: { id: { in: productIds } } + }) + .then(products => { + let sum = 0; + products.forEach(product => { + product.total = product.price * quantities[product.id]; + sum += product.total; + product.quantity = quantities[product.id]; + }); + + resolve({ products, sum }); + }); + }); + + return p; + }, + + findStates: () => { + return models.State.findAll(); + } +}; + +function parseParams(params, query) { + params.where = {}; + + if (query.search) { + params.where["$or"] = [ + { name: { $iLike: `%${query.search}%` } }, + { description: { $iLike: `%${query.search}%` } } + ]; + } + + if (query.category.length) { + params.where["categoryId"] = query.category; + } + + params.where["price"] = { + $gte: query.minPrice, + $lte: query.maxPrice + }; + + if (query.sortBy) { + const sort = query.sortBy.split("-"); + params.order = [[sort[0], sort[1]]]; + } + + return params; +} diff --git a/data_structure.txt b/data_structure.txt new file mode 100644 index 0000000..11f3aab --- /dev/null +++ b/data_structure.txt @@ -0,0 +1,31 @@ +// SQL + +User + firstName + lastName + email + password + street + city + state + +Product + name + sku + description + price + categoryId + inStock + +Category + name + + + +// MongoDB + +Order + productIds + dateOfTransaction + userId + transactionTotal \ No newline at end of file diff --git a/migrations/sequelize/20170809155632-create-product.js b/migrations/sequelize/20170809155632-create-product.js new file mode 100644 index 0000000..24176d0 --- /dev/null +++ b/migrations/sequelize/20170809155632-create-product.js @@ -0,0 +1,44 @@ +"use strict"; +module.exports = { + up: function(queryInterface, Sequelize) { + return queryInterface.createTable("Products", { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + name: { + type: Sequelize.STRING + }, + sku: { + type: Sequelize.STRING + }, + description: { + type: Sequelize.TEXT + }, + price: { + type: Sequelize.FLOAT + }, + categoryId: { + type: Sequelize.INTEGER + }, + inStock: { + type: Sequelize.INTEGER + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.fn("NOW") + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.fn("NOW") + } + }); + }, + down: function(queryInterface, Sequelize) { + return queryInterface.dropTable("Products"); + } +}; diff --git a/migrations/sequelize/20170809155846-create-category.js b/migrations/sequelize/20170809155846-create-category.js new file mode 100644 index 0000000..4892362 --- /dev/null +++ b/migrations/sequelize/20170809155846-create-category.js @@ -0,0 +1,29 @@ +"use strict"; +module.exports = { + up: function(queryInterface, Sequelize) { + return queryInterface.createTable("Categories", { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + name: { + type: Sequelize.STRING + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.fn("NOW") + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.fn("NOW") + } + }); + }, + down: function(queryInterface, Sequelize) { + return queryInterface.dropTable("Categories"); + } +}; diff --git a/migrations/sequelize/20170809195037-unnamed-migration.js b/migrations/sequelize/20170809195037-unnamed-migration.js new file mode 100644 index 0000000..6b113d7 --- /dev/null +++ b/migrations/sequelize/20170809195037-unnamed-migration.js @@ -0,0 +1,26 @@ +"use strict"; + +module.exports = { + up: function(queryInterface, Sequelize) { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.createTable('users', { id: Sequelize.INTEGER }); + */ + + queryInterface.addColumn("Products", "imagePath", Sequelize.STRING); + }, + + down: function(queryInterface, Sequelize) { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.dropTable('users'); + */ + queryInterface.removeColumn("Products", "imagePath"); + } +}; diff --git a/migrations/sequelize/20170811223826-create-state.js b/migrations/sequelize/20170811223826-create-state.js new file mode 100644 index 0000000..8ef3911 --- /dev/null +++ b/migrations/sequelize/20170811223826-create-state.js @@ -0,0 +1,29 @@ +"use strict"; +module.exports = { + up: function(queryInterface, Sequelize) { + return queryInterface.createTable("States", { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + name: { + type: Sequelize.STRING + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.fn("NOW") + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.fn("NOW") + } + }); + }, + down: function(queryInterface, Sequelize) { + return queryInterface.dropTable("States"); + } +}; diff --git a/models/mongoose/index.js b/models/mongoose/index.js new file mode 100644 index 0000000..e193c90 --- /dev/null +++ b/models/mongoose/index.js @@ -0,0 +1,15 @@ +var mongoose = require("mongoose"); +var bluebird = require("bluebird"); + +// Set bluebird as the promise +// library for mongoose +mongoose.Promise = bluebird; + +var models = {}; + +// Load models and attach to models here +models.Order = require("./order"); +models.OrderItem = require("./orderitem"); +//... more models + +module.exports = models; diff --git a/models/mongoose/order.js b/models/mongoose/order.js new file mode 100644 index 0000000..52112ca --- /dev/null +++ b/models/mongoose/order.js @@ -0,0 +1,35 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema; + +const OrderSchema = new Schema( + { + customer: { + fname: String, + lname: String, + email: String, + address: { + street: String, + city: String, + state: String + } + }, + orderItems: [ + { + type: Schema.Types.ObjectId, + ref: "OrderItem" + } + ], + description: String, + total: Number, + token: String, + card: String + }, + { + timestamps: true + } +); + +// Create the model with a defined schema +const Order = mongoose.model("Order", OrderSchema); + +module.exports = Order; diff --git a/models/mongoose/orderitem.js b/models/mongoose/orderitem.js new file mode 100644 index 0000000..081fc98 --- /dev/null +++ b/models/mongoose/orderitem.js @@ -0,0 +1,25 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema; + +const OrderItemSchema = new Schema( + { + productId: String, + name: String, + sku: String, + imagePath: String, + description: String, + price: Number, + categoryId: Number, + category: String, + quantity: Number, + total: Number + }, + { + timestamps: true + } +); + +// Create the model with a defined schema +const OrderItem = mongoose.model("OrderItem", OrderItemSchema); + +module.exports = OrderItem; diff --git a/models/sequelize/category.js b/models/sequelize/category.js new file mode 100644 index 0000000..ddbee3e --- /dev/null +++ b/models/sequelize/category.js @@ -0,0 +1,14 @@ +const models = require("./../sequelize"); + +("use strict"); +module.exports = function(sequelize, DataTypes) { + var Category = sequelize.define("Category", { + name: DataTypes.STRING + }); + + Category.associate = function(models) { + Category.hasMany(models.Product, { foreignKey: "categoryId" }); + }; + + return Category; +}; diff --git a/models/sequelize/index.js b/models/sequelize/index.js new file mode 100644 index 0000000..063b03d --- /dev/null +++ b/models/sequelize/index.js @@ -0,0 +1,43 @@ +"use strict"; + +var fs = require("fs"); +var path = require("path"); +var Sequelize = require("sequelize"); +var basename = path.basename(module.filename); +var env = process.env.NODE_ENV || "development"; +var config = require(__dirname + "./../../config/sequelize.js")[env]; +var db = {}; + +if (config.use_env_variable) { + var sequelize = new Sequelize(process.env[config.use_env_variable]); +} else { + var sequelize = new Sequelize( + config.database, + config.username, + config.password, + config + ); +} + +fs + .readdirSync(__dirname) + .filter(function(file) { + return ( + file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js" + ); + }) + .forEach(function(file) { + var model = sequelize["import"](path.join(__dirname, file)); + db[model.name] = model; + }); + +Object.keys(db).forEach(function(modelName) { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; diff --git a/models/sequelize/product.js b/models/sequelize/product.js new file mode 100644 index 0000000..f899ec1 --- /dev/null +++ b/models/sequelize/product.js @@ -0,0 +1,19 @@ +const models = require("./../sequelize"); + +("use strict"); +module.exports = function(sequelize, DataTypes) { + var Product = sequelize.define("Product", { + name: DataTypes.STRING, + sku: DataTypes.STRING, + imagePath: DataTypes.STRING, + description: DataTypes.TEXT, + price: DataTypes.FLOAT, + categoryId: DataTypes.INTEGER, + inStock: DataTypes.INTEGER + }); + + Product.associate = function(models) { + Product.belongsTo(models.Category, { foreignKey: "categoryId" }); + }; + return Product; +}; diff --git a/models/sequelize/state.js b/models/sequelize/state.js new file mode 100644 index 0000000..47fdbfc --- /dev/null +++ b/models/sequelize/state.js @@ -0,0 +1,13 @@ +'use strict'; +module.exports = function(sequelize, DataTypes) { + var State = sequelize.define('State', { + name: DataTypes.STRING + }, { + classMethods: { + associate: function(models) { + // associations can be defined here + } + } + }); + return State; +}; \ No newline at end of file diff --git a/mongo.js b/mongo.js new file mode 100644 index 0000000..e88f3bc --- /dev/null +++ b/mongo.js @@ -0,0 +1,10 @@ +var mongoose = require("mongoose"); +var env = process.env.NODE_ENV || "development"; +var config = require("./config/mongo")[env]; + +module.exports = () => { + var envUrl = process.env[config.use_env_variable]; + var localUrl = `mongodb://${config.host}/${config.database}`; + var mongoUrl = envUrl ? envUrl : localUrl; + return mongoose.connect(mongoUrl); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..d894c41 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "project_mimirs_market", + "version": "1.0.0", + "description": "An e-commerce app", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "sql-seed": "sequelize db:migrate:undo:all && sequelize db:migrate && sequelize db:seed:all" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Avonyel/project_mimirs_market.git" + }, + "author": "Greg and Stephanie", + "license": "ISC", + "bugs": { + "url": "https://github.com/Avonyel/project_mimirs_market/issues" + }, + "homepage": "https://github.com/Avonyel/project_mimirs_market#readme", + "dependencies": { + "bluebird": "^3.5.0", + "body-parser": "^1.17.2", + "dotenv": "^4.0.0", + "express": "^4.15.4", + "express-handlebars": "^3.0.0", + "express-method-override-get-post-support": "0.0.7", + "express-session": "^1.15.5", + "faker": "^4.1.0", + "method-override": "^2.3.9", + "moment": "^2.18.1", + "moment-timezone": "^0.5.13", + "mongoose": "^4.11.6", + "mongooseeder": "^2.0.5", + "pg": "^7.1.0", + "pg-hstore": "^2.3.2", + "sequelize": "^4.4.2", + "sequelize-cli": "^2.8.0", + "stripe": "^4.24.0" + } +} diff --git a/public/images/icons/calculator.svg b/public/images/icons/calculator.svg new file mode 100644 index 0000000..cacbd42 --- /dev/null +++ b/public/images/icons/calculator.svg @@ -0,0 +1,76 @@ + +image/svg+xml \ No newline at end of file diff --git a/public/images/icons/cart.svg b/public/images/icons/cart.svg new file mode 100644 index 0000000..5f64512 --- /dev/null +++ b/public/images/icons/cart.svg @@ -0,0 +1,44 @@ + +image/svg+xml \ No newline at end of file diff --git a/public/images/icons/dashboard.svg b/public/images/icons/dashboard.svg new file mode 100644 index 0000000..dc4a06a --- /dev/null +++ b/public/images/icons/dashboard.svg @@ -0,0 +1,61 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/public/images/image_0.jpg b/public/images/image_0.jpg new file mode 100644 index 0000000..305619f Binary files /dev/null and b/public/images/image_0.jpg differ diff --git a/public/images/image_1.jpg b/public/images/image_1.jpg new file mode 100644 index 0000000..e557399 Binary files /dev/null and b/public/images/image_1.jpg differ diff --git a/public/images/image_2.jpg b/public/images/image_2.jpg new file mode 100644 index 0000000..afd17c0 Binary files /dev/null and b/public/images/image_2.jpg differ diff --git a/public/images/image_3.jpg b/public/images/image_3.jpg new file mode 100644 index 0000000..9c8e8d6 Binary files /dev/null and b/public/images/image_3.jpg differ diff --git a/public/images/image_4.jpg b/public/images/image_4.jpg new file mode 100644 index 0000000..01b4ab5 Binary files /dev/null and b/public/images/image_4.jpg differ diff --git a/public/images/image_5.jpg b/public/images/image_5.jpg new file mode 100644 index 0000000..c2c4843 Binary files /dev/null and b/public/images/image_5.jpg differ diff --git a/public/images/image_6.jpg b/public/images/image_6.jpg new file mode 100644 index 0000000..214bffa Binary files /dev/null and b/public/images/image_6.jpg differ diff --git a/public/images/image_7.jpg b/public/images/image_7.jpg new file mode 100644 index 0000000..c35e270 Binary files /dev/null and b/public/images/image_7.jpg differ diff --git a/public/images/image_8.jpg b/public/images/image_8.jpg new file mode 100644 index 0000000..71255dd Binary files /dev/null and b/public/images/image_8.jpg differ diff --git a/public/images/image_9.jpg b/public/images/image_9.jpg new file mode 100644 index 0000000..0ec1f16 Binary files /dev/null and b/public/images/image_9.jpg differ diff --git a/public/stylesheets/styles.css b/public/stylesheets/styles.css new file mode 100644 index 0000000..184e845 --- /dev/null +++ b/public/stylesheets/styles.css @@ -0,0 +1,3 @@ +.icon { + max-width: 50px; +} diff --git a/routes/admin.js b/routes/admin.js new file mode 100644 index 0000000..65f5b19 --- /dev/null +++ b/routes/admin.js @@ -0,0 +1,100 @@ +const express = require("express"); +const router = express.Router(); +const mdbModels = require("./../models/mongoose"); +const mongoose = require("mongoose"); +const Order = mongoose.model("Order"); +const OrderItem = mongoose.model("OrderItem"); + +router.get("/", (req, res) => { + Order.find({}) + .then(orders => { + return res.render("dashboard", { orders }); + }) + .catch(e => res.status(500).send(e.stack)); +}); + +router.get("/analytics", (req, res) => { + let output = { + total: {}, + products: [], + states: [] + }; + + Order.aggregate({ + $group: { + _id: null, + total: { $sum: "$total" }, + count: { $sum: 1 }, + uniqueStates: { $addToSet: "$customer.address.state" } + } + }) + .then(total => { + output.total.total = total[0].total; + output.total.transactions = total[0].count; + output.total.states = total[0].uniqueStates.length; + return OrderItem.aggregate({ + $group: { + _id: null, + units: { $sum: "$quantity" }, + uniqueProducts: { $addToSet: "$productId" }, + uniqueCategories: { $addToSet: "$categoryId" } + } + }); + }) + .then(total => { + output.total.units = total[0].units; + output.total.products = total[0].uniqueProducts.length; + output.total.categories = total[0].uniqueCategories.length; + return OrderItem.aggregate([ + { + $group: { + _id: "$name", + total: { $sum: "$total" } + } + }, + { $sort: { total: -1 } }, + { $limit: 10 } + ]); + }) + .then(total => { + output.products = total; + return Order.aggregate([ + { + $group: { + _id: "$customer.address.state", + total: { $sum: "$total" } + } + }, + { $sort: { total: -1 } }, + { $limit: 10 } + ]); + }) + .then(total => { + output.states = total; + return OrderItem.aggregate([ + { + $group: { + _id: "$category", + total: { $sum: "$total" } + } + }, + { $sort: { total: -1 } }, + { $limit: 10 } + ]); + }) + .then(total => { + output.categories = total; + return res.render("analytics", output); + }); +}); + +router.get("/:id", (req, res) => { + Order.findById(req.params.id) + .populate("orderItems") + .then(order => { + return res.render("singleOrder", { order }); + }) + .catch(e => res.status(500).send(e.stack)); +}); + +module.exports = router; diff --git a/routes/cart.js b/routes/cart.js new file mode 100644 index 0000000..da8038d --- /dev/null +++ b/routes/cart.js @@ -0,0 +1,44 @@ +const express = require("express"); +const router = express.Router(); +const ProductsController = require("./../controllers/products"); + +router.get("/", (req, res) => { + ProductsController.showCart(req, res).then(cart => { + res.render("cart", { products: cart.products, sum: cart.sum }); + }); +}); + +router.post("/:id/quantity", (req, res) => { + if (isNaN(Number(req.body.quantity))) { + res.redirect("back"); + } + + if (Number(req.body.quantity) <= 0) { + removeSingle(req, res); + } + + req.session.cart.forEach(item => { + if (item.id === req.params.id) { + item.quantity = req.body.quantity; + } + }); + + res.redirect("back"); +}); + +router.post("/:id/remove", removeSingle); + +router.post("/empty", (req, res) => { + req.session.cart = []; + res.redirect("back"); +}); + +function removeSingle(req, res) { + req.session.cart = req.session.cart.filter(el => { + return el.id !== req.params.id; + }); + + res.redirect("back"); +} + +module.exports = router; diff --git a/routes/checkout.js b/routes/checkout.js new file mode 100644 index 0000000..f6c7cf6 --- /dev/null +++ b/routes/checkout.js @@ -0,0 +1,96 @@ +const express = require("express"); +const router = express.Router(); +const ProductsController = require("./../controllers/products"); +const stripe = require("stripe")(process.env.STRIPE_SK); +const mongoose = require("mongoose"); +const mdbModels = require("./../models/mongoose"); + +router.get("/", (req, res) => { + output = {}; + ProductsController.showCart(req, res) + .then(cart => { + output.products = cart.products; + output.sum = cart.sum; + output.STRIPE_PK = process.env.STRIPE_PK; + + return ProductsController.findStates(); + }) + .then(states => { + output.states = states; + res.render("checkout", output); + }); +}); + +router.post("/charge", (req, res) => { + let charge = req.body; + let order = {}; + let orderItems = []; + order.customer = { + fname: req.body.fname, + lname: req.body.lname, + email: req.body.email, + address: { + street: req.body.street, + city: req.body.city, + state: req.body.state + } + }; + + order.orderItems = []; + + ProductsController.showCart(req, res) + .then(cart => { + order.description = `Bought ${Object.keys(cart.products).length} item(s)`; + cart.products.forEach(product => { + product = new mdbModels.OrderItem({ + id: product.id, + name: product.name, + sku: product.sku, + imagePath: product.imagePath, + description: product.description, + price: product.price, + categoryId: product.categoryId, + category: product.Category.name, + createdAt: product.createdAt, + updatedAt: product.updatedAt, + quantity: product.quantity, + total: product.price * product.quantity + }); + + order.orderItems.push(product); + orderItems.push(product); + }); + order.total = cart.sum; + + return stripe.charges.create({ + amount: cart.sum * 100, + currency: "usd", + description: `Bought ${Object.keys(cart.products).length} item(s)`, + source: charge.stripeToken + }); + }) + .then(newCharge => { + console.log(newCharge); + (order.token = newCharge.balance_transaction), (order.card = + newCharge.source.brand); + + newOrder = new mdbModels.Order(order); + + let promises = []; + + promises.push(newOrder.save()); + + orderItems.forEach(model => { + promises.push(model.save()); + }); + + return Promise.all(promises); + }) + .then(() => { + req.session.cart = []; + res.redirect("/"); + }) + .catch(e => res.status(500).send(e.stack)); +}); + +module.exports = router; diff --git a/routes/products.js b/routes/products.js new file mode 100644 index 0000000..e3c2563 --- /dev/null +++ b/routes/products.js @@ -0,0 +1,17 @@ +const express = require("express"); +const router = express.Router(); +const ProductsController = require("./../controllers/products"); + +router.get("/", (req, res) => { + res.redirect("/"); +}); + +router.get("/:id", ProductsController.singleProduct); + +router.post("/:id", (req, res) => { + req.session.cart = req.session.cart || []; + req.session.cart.push({ id: req.params.id, quantity: 1 }); + res.redirect("back"); +}); + +module.exports = router; diff --git a/seeds/mongoose/index.js b/seeds/mongoose/index.js new file mode 100644 index 0000000..f3800b7 --- /dev/null +++ b/seeds/mongoose/index.js @@ -0,0 +1,70 @@ +const mongoose = require("mongoose"); +const mongooseeder = require("mongooseeder"); +var env = process.env.NODE_ENV || "development"; +const models = require("./../../models/mongoose"); +const faker = require("faker"); +var config = require("./../../config/mongo")[env]; +const { Order, OrderItem } = models; + +const mongodbUrl = + process.env.NODE_ENV === "production" + ? process.env[config.use_env_variable] + : `mongodb://${config.host}/${config.database}`; + +mongooseeder.seed({ + mongodbUrl: mongodbUrl, + models: models, + clean: true, + mongoose: mongoose, + seeds: () => { + // Run your seeds here + let orders = []; + let orderItems = []; + for (let i = 0; i < 10; i++) { + let order = new Order({ + customer: { + fname: faker.name.firstName(), + lname: faker.name.lastName(), + email: faker.internet.email(), + address: { + street: faker.address.streetName(), + city: faker.address.city(), + state: faker.address.state() + } + }, + description: faker.lorem.words(), + total: 0, + token: `tok_XXXXXXXXXXXXX${i}`, + card: "Visa" + }); + + for (let j = 1; j < 4; j++) { + let orderItem = new OrderItem({ + productId: j, + sku: `FKP12345N${i}`, + name: faker.commerce.productName(), + imagePath: `image_${i % 10}.jpg`, + description: faker.lorem.sentences(), + price: faker.commerce.price(), + categoryId: Math.floor(Math.random() * 10 + 1), + category: faker.commerce.department(), + quantity: Math.floor(Math.random() * 5 + 1) + }); + + orderItem.total = orderItem.price * orderItem.quantity; + order.total += Number(orderItem.total); + orderItems.push(orderItem); + order.orderItems.push(orderItem); + } + orders.push(order); + } + + let promises = []; + [orders, orderItems].forEach(collection => { + collection.forEach(model => { + promises.push(model.save()); + }); + }); + return Promise.all(promises); + } +}); diff --git a/seeds/sequelize/20170809162706-products.js b/seeds/sequelize/20170809162706-products.js new file mode 100644 index 0000000..aa4a773 --- /dev/null +++ b/seeds/sequelize/20170809162706-products.js @@ -0,0 +1,43 @@ +const models = require("./../../models/sequelize"); +const faker = require("faker"); + +("use strict"); + +module.exports = { + up: function(queryInterface, Sequelize) { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkInsert('Person', [{ + name: 'John Doe', + isBetaMember: false + }], {}); + */ + let products = []; + for (let i = 0; i < 100; i++) { + products.push({ + sku: `FKP12345N${i}`, + name: faker.commerce.productName(), + imagePath: `image_${i % 10}.jpg`, + description: faker.lorem.sentences(), + price: faker.commerce.price(), + categoryId: Math.floor(Math.random() * 10 + 1), + inStock: Math.floor(Math.random() * 50 + 1) + }); + } + return queryInterface.bulkInsert("Products", products); + }, + + down: function(queryInterface, Sequelize) { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkDelete('Person', null, {}); + */ + return queryInterface.bulkDelete("Products", null, {}, models.Products); + } +}; diff --git a/seeds/sequelize/20170809164059-categories.js b/seeds/sequelize/20170809164059-categories.js new file mode 100644 index 0000000..48167ce --- /dev/null +++ b/seeds/sequelize/20170809164059-categories.js @@ -0,0 +1,36 @@ +"use strict"; +const models = require("./../../models/sequelize"); +const faker = require("faker"); + +module.exports = { + up: function(queryInterface, Sequelize) { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkInsert('Person', [{ + name: 'John Doe', + isBetaMember: false + }], {}); + */ + let categories = []; + for (let i = 0; i < 10; i++) { + categories.push({ + name: faker.commerce.department() + }); + } + return queryInterface.bulkInsert("Categories", categories); + }, + + down: function(queryInterface, Sequelize) { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkDelete('Person', null, {}); + */ + return queryInterface.bulkDelete("Categories", null, {}, models.Categories); + } +}; diff --git a/seeds/sequelize/20170811224116-states.js b/seeds/sequelize/20170811224116-states.js new file mode 100644 index 0000000..a8684ea --- /dev/null +++ b/seeds/sequelize/20170811224116-states.js @@ -0,0 +1,85 @@ +"use strict"; + +module.exports = { + up: function(queryInterface, Sequelize) { + /* + Add altering commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkInsert('Person', [{ + name: 'John Doe', + isBetaMember: false + }], {}); + */ + let allStates = [ + "Alabama", + "Alaska", + "Arizona", + "Arkansas", + "California", + "Colorado", + "Connecticut", + "Delaware", + "Florida", + "Georgia", + "Hawaii", + "Idaho", + "Illinois", + "Indiana", + "Iowa", + "Kansas", + "Kentucky", + "Louisiana", + "Maine", + "Maryland", + "Massachusetts", + "Michigan", + "Minnesota", + "Mississippi", + "Missouri", + "Montana", + "Nebraska", + "Nevada", + "New Hampshire", + "New Jersey", + "New Mexico", + "New York", + "North Carolina", + "North Dakota", + "Ohio", + "Oklahoma", + "Oregon", + "Pennsylvania", + "Rhode Island", + "South Carolina", + "South Dakota", + "Tennessee", + "Texas", + "Utah", + "Vermont", + "Virginia", + "Washington", + "West Virginia", + "Wisconsin", + "Wyoming" + ]; + + allStates = allStates.map(state => { + return { name: state }; + }); + + return queryInterface.bulkInsert("States", allStates); + }, + + down: function(queryInterface, Sequelize) { + /* + Add reverting commands here. + Return a promise to correctly handle asynchronicity. + + Example: + return queryInterface.bulkDelete('Person', null, {}); + */ + return queryInterface.bulkDelete("State", null, {}); + } +}; diff --git a/views/analytics.handlebars b/views/analytics.handlebars new file mode 100644 index 0000000..07df2df --- /dev/null +++ b/views/analytics.handlebars @@ -0,0 +1,64 @@ +

Admin Analytics

+ +
+
+

Totals

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Total Revenue Ever${{total.total}}.00
Total Units Sold{{total.units}}
Total Transactions{{total.transactions}}
Total Products{{total.products}}
Total Categories{{total.categories}}
Total States Sold To{{total.states}}
+ +

Revenue by Product

+ + {{#each products as |product|}} + + + + + {{/each}} +
{{product._id}}${{product.total}}.00
+
+
+

Revenue by State

+ + {{#each states as |state|}} + + + + + {{/each}} +
{{state._id}}${{state.total}}.00
+ +

Revenue by Category

+ + {{#each categories as |category|}} + + + + + {{/each}} +
{{category._id}}${{category.total}}.00
+
+
\ No newline at end of file diff --git a/views/cart.handlebars b/views/cart.handlebars new file mode 100644 index 0000000..96dd4e7 --- /dev/null +++ b/views/cart.handlebars @@ -0,0 +1,50 @@ +

Cart

+ +
+
+ {{#each products as |product|}} +
+
+ +
+
+

{{product.name}}

+

{{product.Category.name}}

+

${{product.price}}.00

+
+
+
+ +
+
+ + +
+
+ +
+
+
+ {{/each}} +
+ {{#if products.length}} +
+

Total: ${{sum}}.00

+
+
+
+ +
+
+
+ Checkout +
+ {{else}} +
+

There are no items in your cart.

+
+ {{/if}} +
+
+
+ diff --git a/views/checkout.handlebars b/views/checkout.handlebars new file mode 100644 index 0000000..30150b3 --- /dev/null +++ b/views/checkout.handlebars @@ -0,0 +1,82 @@ +

Checkout

+ +
+
+
+

Billing Info

+
+ Personal +
+ + +
+
+ + +
+
+ + +
+
+
+ Address +
+ + +
+
+ + +
+
+ + +
+
+
+
+

Order Items

+ {{#each products as |product|}} +
+
+ +
+
+

{{product.name}}

+
+
+

${{product.price}}.00 x {{product.quantity}} = ${{product.total}}.00

+
+
+ {{/each}} + +
+
+

Total: ${{sum}}.00

+
+
+ +
+
+ +
+ + + +
+
+
\ No newline at end of file diff --git a/views/dashboard.handlebars b/views/dashboard.handlebars new file mode 100644 index 0000000..6189819 --- /dev/null +++ b/views/dashboard.handlebars @@ -0,0 +1,26 @@ +

Admin Orders Index

+ + + + + + + + + + + + + + {{#each orders as |order|}} + + + + + + + + + {{/each}} + +
IDDescriptionCheckout DateRevenueCustomer EmailCustomer State
{{order.id}}{{order.description}}{{order.createdAt}}${{order.total}}.00{{order.customer.email}}{{order.customer.state}}
\ No newline at end of file diff --git a/views/index.handlebars b/views/index.handlebars new file mode 100644 index 0000000..ebc34ed --- /dev/null +++ b/views/index.handlebars @@ -0,0 +1,96 @@ +
+ +
+
+ + +
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+ + +


+ +
+ {{#each products as |product|}} +
+ +

${{product.price}}.00

+

{{product.name}}

+

{{product.Category.name}}

+ + {{#if product.inCart}} + Edit in Cart + {{else}} +
+ +
+ {{/if}} +
+ {{/each}} +
diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars new file mode 100644 index 0000000..eab6729 --- /dev/null +++ b/views/layouts/main.handlebars @@ -0,0 +1,39 @@ + + + + Mimir's Market + + + + + + +
+ +
+ +
+ {{{body}}} +
+ + diff --git a/views/product.handlebars b/views/product.handlebars new file mode 100644 index 0000000..1149f92 --- /dev/null +++ b/views/product.handlebars @@ -0,0 +1,40 @@ +
+
+ +
+
+

${{product.price}}.00

+

{{product.name}}

+

{{product.Category.name}}

+
+
+

{{product.description}}

+ {{#if product.inCart}} + Edit in Cart + {{else}} +
+ +
+ {{/if}} +
+
+ +

Related Products

+ +
+ {{#each relatedProducts as |rProduct|}} +
+ +

${{rProduct.price}}.00

+

{{rProduct.name}}

+

{{rProduct.Category.name}}

+ {{#if rProduct.inCart}} + Edit in Cart + {{else}} +
+ +
+ {{/if}} +
+ {{/each}} +
diff --git a/views/singleOrder.handlebars b/views/singleOrder.handlebars new file mode 100644 index 0000000..0b0cc7a --- /dev/null +++ b/views/singleOrder.handlebars @@ -0,0 +1,109 @@ +

Admin Orders Show

+ +
+
+

Ordered Items

+ {{#each order.orderItems as |product|}} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldValue
Name{{product.name}}
Price${{product.price}}.00
SKU{{product.sku}}
Description{{product.description}}
Category{{product.category}}
Quantity{{product.quantity}}
+
+ {{/each}} +
+
+

Customer Info

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldValue
First Name{{order.customer.fname}}
Last Name{{order.customer.lname}}
Email{{order.customer.email}}
Street{{order.customer.address.street}}
City{{order.customer.address.city}}
State{{order.customer.address.state}}
+
+

Order Info

+
+ + + + + + + + + + + + + + + + + + + + + +
FieldValue
Checkout Date{{order.createdAt}}
Balance Transaction{{order.token}}
Card Type{{order.card}}
+
+
+
\ No newline at end of file diff --git a/views/testing.handlebars b/views/testing.handlebars new file mode 100644 index 0000000..86f3e86 --- /dev/null +++ b/views/testing.handlebars @@ -0,0 +1,3 @@ +{{#each users as |user|}} +

{{user.email}}

+{{/each}} \ No newline at end of file