├── .gitignore ├── README.md ├── app.js ├── bin └── www ├── package.json ├── public ├── images │ ├── loading.gif │ └── logo.png ├── javascripts │ ├── app.js │ ├── global.js │ └── jquery.form.js ├── libraries │ ├── imageviewer │ │ ├── imageviewer.css │ │ └── imageviewer.min.js │ └── lobibox │ │ ├── lobibox.min.css │ │ └── lobibox.min.js └── stylesheets │ └── style.css ├── routes ├── index.js ├── product.js ├── user.js └── user_product.js └── views ├── error.jade ├── front ├── index.jade ├── products-single.jade └── products.jade ├── layout.jade ├── partials ├── footer.jade └── header.jade └── user ├── account.jade ├── login.jade ├── products-add.jade ├── products-edit.jade ├── products.jade └── register.jade /.gitignore: -------------------------------------------------------------------------------- 1 | /data/ 2 | /node_modules/ 3 | /public/images/uploads/ 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js E-commerce 2 | Companies nowadays are now turning their businesses online to provide their customers a better experience. One of the most popular e-commerce website we hear often is Ebay – an American multinational company running on Node.js . A lot of folks are suggesting that Node.js might be the future of web development. Node.js is a single-threaded event-driven system that runs fast even when handling lots of requests at once, it is also simple compared to traditional multi-threaded frameworks. Node.js is well suited for real-time applications: online games, collaboration tools, chat rooms, or anything where what one user (or robot?) does with the application needs to be seen by other users immediately, without a page refresh. Using a technique known as “long-polling”, you can write an application that sends updates to the user in real time. 3 | 4 | ## Demo 5 | This simple e-commerce application demonstrates CRUD operations using mongoDB and a simple implementation of a session based user authentication using Passport.js. It also utilizes real time update front-end products using socket.io. This web app is 100% free to use, you can customize it to build a more sophisticated e-commerce web application. Feel free to submit an issue on GitHub if you found any bug or even better – submit a pull request. 6 | 7 | This web application is currently hosted on Heroku, [click here](https://nodejs-ecommerce.herokuapp.com/) to view the demo. Heroku is for testing only - it does not support file uploads as the filesystem is readonly. Although you can still test the image upload functionality but all images will automatically be deleted after a couple of minutes. 8 | 9 | ## Modules Used 10 | + async 11 | + connect-flash 12 | + cookie-parser 13 | + crypto-md5 14 | + express 15 | + express-session 16 | + jade 17 | + mongodb 18 | + monk 19 | + multer 20 | + passport 21 | + passport-local 22 | + socket.io 23 | 24 | ## How do I get setup? 25 | 1. Create a folder called "data" in the root directory of our nodejs project. This is where MongoDB documents will be stored. 26 | 2. Go to MongoDB installation directory and under the bin folder run this command: `mongod --dbpath C:\Users\Carl\Documents\nodejs-ecommerce\data` This will start the MongoDB server. Leave this CLI instance open and start another CLI instance. 27 | 3. In the new CLI, navigate to where you pulled this repository, ex. `C:\Users\Carl\Documents\nodejs-ecommerce`, then type-in: npm install then wait till it finishes installing all the modules required to run our Node.js Web Application. 28 | 4. Once the installation is completed, type in the following command to run our Web Application: npm start Make sure to keep the CLI opened. 29 | 5. Now go to [http://127.0.0.1:3100/](http://127.0.0.1:3100/) using your favorite browser. 30 | 31 | ## Contribution guidelines 32 | + ALWAYS start a new branch before making changes to the codes 33 | + Pull requests directly to the master branch will be ignored! 34 | + Use a git client, preferably Source Tree or you can use git commands from your terminal, your choice! 35 | + Many smaller commits are better than one large commit. Edit your file(s), if the edit does not break the code with things like syntax errors, commit. It is easier to reconcile many smaller commits than one large commit. 36 | + When your feature or bug fix is ready, perform a pull request and notify carl.fontanos@gmail.com that your code is ready for review on Github. 37 | 38 | ## Author 39 | ### Carl Victor C. Fontanos 40 | + Website: [carlofontanos.com](http://www.carlofontanos.com) 41 | + Linkedin: [ph.linkedin.com/in/carlfontanos](http://ph.linkedin.com/in/carlfontanos) 42 | + Facebook: [facebook.com/carlo.fontanos](http://facebook.com/carlo.fontanos) 43 | + Twitter: [twitter.com/carlofontanos](http://twitter.com/carlofontanos) 44 | + Google+: [plus.google.com/u/0/107219338853998242780/about](https://plus.google.com/u/0/107219338853998242780/about) 45 | + GitHub: [github.com/carlo-fontanos](https://github.com/carlo-fontanos) -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var session = require('express-session'); 3 | var path = require('path'); 4 | var favicon = require('serve-favicon'); 5 | var logger = require('morgan'); 6 | var cookieParser = require('cookie-parser'); 7 | var bodyParser = require('body-parser'); 8 | var passport = require('passport'); 9 | var LocalStrategy = require('passport-local').Strategy; 10 | var flash = require('connect-flash'); 11 | var md5 = require('crypto-md5'); 12 | 13 | /* MongoDB connection */ 14 | var mongo = require('mongodb'); 15 | var monk = require('monk'); 16 | var db = monk('localhost:27017/nodetest1'); // 27017 is the default port for our MongoDB instance. 17 | var users = db.get('users'); 18 | 19 | var routes = require('./routes/index'); 20 | 21 | var app = express(); 22 | 23 | /* Setup socket.io and express to run on same port (3100) */ 24 | var server = require('http').Server(app); 25 | var io = require('socket.io')(server); 26 | 27 | server.listen(3100); 28 | 29 | /* Realtime trigger */ 30 | io.sockets.on('connection', function (socket) { 31 | socket.on('send', function (data) { 32 | io.sockets.emit('message', data); 33 | }); 34 | }); 35 | 36 | /* Define some globals that will be made accessible all through out the application */ 37 | global.root_dir = path.resolve(__dirname); 38 | global.uploads_dir = root_dir + '/public/images/uploads/'; 39 | 40 | /* view engine setup */ 41 | app.set('views', path.join(__dirname, 'views')); 42 | app.set('view engine', 'jade'); 43 | 44 | /* Make the response uncompressed */ 45 | app.locals.pretty = true; 46 | 47 | /* uncomment after placing your favicon in /public */ 48 | /* app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); */ 49 | app.use(logger('dev')); 50 | app.use(bodyParser.json()); 51 | app.use(bodyParser.urlencoded({ extended: false })); 52 | app.use(cookieParser()); 53 | app.use(express.static(path.join(__dirname, 'public'))); 54 | app.use(flash()); 55 | app.use(session({ 56 | secret: 'secret cat', 57 | resave: true, 58 | saveUninitialized: true 59 | })); 60 | app.use(passport.initialize()); 61 | app.use(passport.session()); 62 | 63 | passport.use(new LocalStrategy({ 64 | /* Define custom fields for passport */ 65 | usernameField : 'email', 66 | passwordField : 'password' 67 | }, 68 | function(email, password, done) { 69 | /* validate email and password */ 70 | users.findOne({email: email}, function(err, user) { 71 | if (err) { return done(err); } 72 | if (!user) { 73 | return done(null, false, {message: 'Incorrect username.'}); 74 | } 75 | if (user.password != md5(password)) { 76 | return done(null, false, {message: 'Incorrect password.'}); 77 | } 78 | /* if everything is correct, let's pass our user object to the passport.serializeUser */ 79 | return done(null, user); 80 | }); 81 | } 82 | )); 83 | 84 | passport.serializeUser(function(user, done) { 85 | /* Attach to the session as req.session.passport.user = { email: 'test@test.com' } */ 86 | /* The email key will be later used in our passport.deserializeUser function */ 87 | done(null, user.email); 88 | }); 89 | 90 | passport.deserializeUser(function(email, done) { 91 | users.findOne({email: email}, function(err, user) { 92 | /* The fetched "user" object will be attached to the request object as req.user */ 93 | done(err, user); 94 | }); 95 | }); 96 | 97 | 98 | app.use(function(req, res, next){ 99 | req.db = db; /* Make our db accessible to our router */ 100 | res.locals.user = req.user; /* Make our user object accessible in all our templates. */ 101 | next(); 102 | }); 103 | 104 | app.use('/', routes); 105 | 106 | /* catch 404 and forward to error handler */ 107 | app.use(function(req, res, next) { 108 | var err = new Error('Not Found'); 109 | err.status = 404; 110 | next(err); 111 | }); 112 | 113 | /* error handlers */ 114 | 115 | /* development error handler */ 116 | /* will print stacktrace */ 117 | if (app.get('env') === 'development') { 118 | app.use(function(err, req, res, next) { 119 | res.status(err.status || 500); 120 | res.render('error', { 121 | message: err.message, 122 | error: err 123 | }); 124 | }); 125 | } 126 | 127 | /* production error handler */ 128 | /* no stacktraces leaked to user */ 129 | app.use(function(err, req, res, next) { 130 | res.status(err.status || 500); 131 | res.render('error', { 132 | message: err.message, 133 | error: {} 134 | }); 135 | }); 136 | 137 | 138 | module.exports = app; -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('nodetest1:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodetest1", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "async": "^2.0.1", 10 | "body-parser": "~1.12.4", 11 | "connect-flash": "^0.1.1", 12 | "cookie-parser": "~1.3.5", 13 | "crypto-md5": "^1.0.0", 14 | "debug": "~2.2.0", 15 | "express": "~4.12.4", 16 | "express-session": "^1.14.1", 17 | "jade": "~1.9.2", 18 | "mongodb": "^1.4.4", 19 | "monk": "^1.0.1", 20 | "morgan": "~1.5.3", 21 | "multer": "^1.2.0", 22 | "passport": "^0.3.2", 23 | "passport-local": "^1.0.0", 24 | "serve-favicon": "~2.2.1", 25 | "socket.io": "^1.4.8" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /public/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomMT/node.js-e-commerce/d52e0eddf77c9089fad374fa98c2b4e1be9fcf39/public/images/loading.gif -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomMT/node.js-e-commerce/d52e0eddf77c9089fad374fa98c2b4e1be9fcf39/public/images/logo.png -------------------------------------------------------------------------------- /public/javascripts/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * App Class 3 | * 4 | * @author Carl Victor Fontanos 5 | * @author_url www.carlofontanos.com 6 | * 7 | */ 8 | 9 | /** 10 | * Setup a App namespace to prevent JS conflicts. 11 | */ 12 | 13 | var socket = io.connect('http://localhost:3100'); 14 | var app = { 15 | 16 | 17 | Posts: function() { 18 | 19 | /** 20 | * This method contains the list of functions that needs to be loaded 21 | * when the "Posts" object is instantiated. 22 | * 23 | */ 24 | this.init = function() { 25 | // this.loaded_posts_pagination(); 26 | this.get_all_items_pagination(); 27 | this.get_user_items_pagination(); 28 | this.add_post(); 29 | this.update_post(); 30 | this.delete_post(); 31 | this.unset_image(); 32 | this.set_featured_image(); 33 | this.set_imageviewer(); 34 | } 35 | 36 | /** 37 | * Load user items pagination. 38 | */ 39 | this.get_user_items_pagination = function() { 40 | 41 | _this = this; 42 | 43 | /* Check if our hidden form input is not empty, meaning it's not the first time viewing the page. */ 44 | if($('form.post-list input').val()){ 45 | /* Submit hidden form input value to load previous page number */ 46 | data = JSON.parse($('form.post-list input').val()); 47 | _this.ajax_get_user_items_pagination(data.page, data.th_name, data.th_sort); 48 | } else { 49 | /* Load first page */ 50 | _this.ajax_get_user_items_pagination(1, 'name', 'ASC'); 51 | } 52 | 53 | var th_active = $('.table-post-list th.active'); 54 | var th_name = $(th_active).attr('id'); 55 | var th_sort = $(th_active).hasClass('DESC') ? 'DESC': 'ASC'; 56 | 57 | /* Search */ 58 | $('body').on('click', '.post_search_submit', function(){ 59 | _this.ajax_get_user_items_pagination(1, th_name, th_sort); 60 | }); 61 | /* Search when Enter Key is triggered */ 62 | $(".post_search_text").keyup(function (e) { 63 | if (e.keyCode == 13) { 64 | _this.ajax_get_user_items_pagination(1, th_name, th_sort); 65 | } 66 | }); 67 | 68 | /* Pagination Clicks */ 69 | $('body').on('click', '.pagination-nav li.active', function(){ 70 | var page = $(this).attr('p'); 71 | var current_th_active = $('.table-post-list th.active'); 72 | var current_sort = $(current_th_active).hasClass('DESC') ? 'DESC': 'ASC'; 73 | var current_name = $(current_th_active).attr('id'); 74 | _this.ajax_get_user_items_pagination(page, current_name, current_sort); 75 | }); 76 | 77 | /* Sorting Clicks */ 78 | $('body').on('click', '.table-post-list th', function(e) { 79 | e.preventDefault(); 80 | var th_name = $(this).attr('id'); 81 | 82 | if(th_name){ 83 | /* Remove all TH tags with an "active" class */ 84 | if($('.table-post-list th').removeClass('active')) { 85 | /* Set "active" class to the clicked TH tag */ 86 | $(this).addClass('active'); 87 | } 88 | if(!$(this).hasClass('DESC')){ 89 | _this.ajax_get_user_items_pagination(1, th_name, 'DESC'); 90 | $(this).addClass('DESC'); 91 | } else { 92 | _this.ajax_get_user_items_pagination(1, th_name, 'ASC'); 93 | $(this).removeClass('DESC'); 94 | } 95 | } 96 | }); 97 | } 98 | 99 | /** 100 | * AJAX user items pagination. 101 | */ 102 | this.ajax_get_user_items_pagination = function(page, th_name, th_sort){ 103 | 104 | if($(".pagination-container").length > 0 && $(".products-view-user").length > 0){ 105 | $(".pagination-container").html(''); 106 | 107 | var post_data = { 108 | page: page, 109 | search: $('.post_search_text').val(), 110 | th_name: th_name, 111 | th_sort: th_sort, 112 | max: $('.post_max').val(), 113 | }; 114 | 115 | $('form.post-list input').val(JSON.stringify(post_data)); 116 | 117 | var data = { 118 | action: "demo_load_my_posts", 119 | data: JSON.parse($('form.post-list input').val()) 120 | }; 121 | 122 | $.ajax({ 123 | url: '/user/products/view', 124 | type: 'POST', 125 | contentType: 'application/json', 126 | data: JSON.stringify(data), 127 | success: function (response) { 128 | 129 | if($(".pagination-container").html(response.content)){ 130 | $('.pagination-nav').html(response.navigation); 131 | $('.table-post-list th').each(function() { 132 | /* Append the button indicator */ 133 | $(this).find('span.glyphicon').remove(); 134 | if($(this).hasClass('active')){ 135 | if(JSON.parse($('form.post-list input').val()).th_sort == 'DESC'){ 136 | $(this).append(' '); 137 | } else { 138 | $(this).append(' '); 139 | } 140 | } 141 | }); 142 | } 143 | } 144 | }); 145 | } 146 | } 147 | 148 | /** 149 | * Load front-end items pagination. 150 | */ 151 | this.get_all_items_pagination = function() { 152 | 153 | _this = this; 154 | 155 | /* Check if our hidden form input is not empty, meaning it's not the first time viewing the page. */ 156 | if($('form.post-list input').val()){ 157 | /* Submit hidden form input value to load previous page number */ 158 | data = JSON.parse($('form.post-list input').val()); 159 | _this.ajax_get_all_items_pagination(data.page, data.name, data.sort); 160 | } else { 161 | /* Load first page */ 162 | _this.ajax_get_all_items_pagination(1, $('.post_name').val(), $('.post_sort').val()); 163 | } 164 | 165 | /* Search */ 166 | $('body').on('click', '.post_search_submit', function(){ 167 | _this.ajax_get_all_items_pagination(1, $('.post_name').val(), $('.post_sort').val()); 168 | }); 169 | /* Search when Enter Key is triggered */ 170 | $(".post_search_text").keyup(function (e) { 171 | if (e.keyCode == 13) { 172 | _this.ajax_get_all_items_pagination(1, $('.post_name').val(), $('.post_sort').val()); 173 | } 174 | }); 175 | 176 | /* Pagination Clicks */ 177 | $('body').on('click', '.pagination-nav li.active', function(){ 178 | var page = $(this).attr('p'); 179 | _this.ajax_get_all_items_pagination(page, $('.post_name').val(), $('.post_sort').val()); 180 | }); 181 | } 182 | 183 | /** 184 | * AJAX front-end items pagination. 185 | */ 186 | this.ajax_get_all_items_pagination = function(page, order_by_name, order_by_sort){ 187 | 188 | if($(".pagination-container").length > 0 && $('.products-view-all').length > 0 ){ 189 | $(".pagination-container").html(''); 190 | 191 | var post_data = { 192 | page: page, 193 | search: $('.post_search_text').val(), 194 | name: order_by_name, 195 | sort: order_by_sort, 196 | max: $('.post_max').val(), 197 | }; 198 | 199 | $('form.post-list input').val(JSON.stringify(post_data)); 200 | 201 | var data = { 202 | action: 'get-all-products', 203 | data: JSON.parse($('form.post-list input').val()) 204 | }; 205 | 206 | $.ajax({ 207 | url: 'products/view-front', 208 | type: 'POST', 209 | contentType: 'application/json', 210 | data: JSON.stringify(data), 211 | success: function (response) { 212 | 213 | if($(".pagination-container").html(response.content)){ 214 | $('.pagination-nav').html(response.navigation); 215 | $('.table-post-list th').each(function() { 216 | /* Append the button indicator */ 217 | $(this).find('span.glyphicon').remove(); 218 | if($(this).hasClass('active')){ 219 | if(JSON.parse($('form.post-list input').val()).th_sort == 'DESC'){ 220 | $(this).append(' '); 221 | } else { 222 | $(this).append(' '); 223 | } 224 | } 225 | }); 226 | } 227 | } 228 | }); 229 | } 230 | } 231 | 232 | /** 233 | * Submit updated data via ajax using jquery form plugin 234 | */ 235 | this.update_post = function(){ 236 | $('.update-product').ajaxForm({ 237 | beforeSerialize: function() { 238 | update_ckeditor_instances(); 239 | wave_box('on'); 240 | }, 241 | success: function(response, textStatus, xhr, form) { 242 | if(response.status == 0){ 243 | if($.isArray(response.errors)){ 244 | $.each(response.errors, function (key, error_nessage) { 245 | Lobibox.notify('error', {msg: error_nessage, size: 'mini', sound: false}); 246 | }); 247 | } 248 | } 249 | if(response.status == 1){ 250 | if(response.message){ 251 | Lobibox.notify('success', {msg: response.message, size: 'mini', sound: false}); 252 | } 253 | 254 | socket.emit('send', { message: $('.update-product').serializeObject() } ); 255 | } 256 | if(response.images){ 257 | $('.image-input').val(''); 258 | $('.no-item-images').remove(); 259 | $.each(response.images, function (index, image) { 260 | $('.images-section').append( 261 | '
' + 262 | '' + 263 | '' + 264 | '' + 265 | '
' 266 | ); 267 | }); 268 | } 269 | wave_box('off'); 270 | } 271 | }); 272 | } 273 | 274 | /** 275 | * Submit new product data via ajax using jquery form plugin 276 | */ 277 | this.add_post = function(){ 278 | $('.create-product').ajaxForm({ 279 | beforeSubmit: function(arr, $form, options) { 280 | var proceed = true; 281 | 282 | $('input.required').each(function(index) { 283 | if($(this).val() == ''){ 284 | Lobibox.notify('error', {msg: 'Please fill-up the required fields', size: 'mini', sound: false}); 285 | proceed = false; 286 | return false; 287 | } 288 | }); 289 | 290 | return proceed; 291 | }, 292 | beforeSerialize: function() { 293 | update_ckeditor_instances(); 294 | }, 295 | success: function(response, textStatus, xhr, form) { 296 | if(response == 0){ 297 | Lobibox.notify('error', {msg: 'Failed to create the product, please try again', size: 'mini', sound: false}); 298 | } else { 299 | window.location.href = '/user/products/edit/' + response + '?status=created'; 300 | } 301 | } 302 | }); 303 | 304 | if(get_url_value('status') == 'created'){ 305 | Lobibox.notify('success', {msg: 'Item successfully created, you may continue editing this product.', size: 'mini', sound: false}); 306 | } 307 | } 308 | 309 | /** 310 | * Handles the deletion of a single post 311 | */ 312 | this.delete_post = function(){ 313 | $('body').on('click', '.delete-product', function(e) { 314 | e.preventDefault(); 315 | 316 | var item = $(this); 317 | var data = { 318 | item_id: item.attr('item_id') 319 | } 320 | 321 | $.ajax({ 322 | url: '/user/products/delete', 323 | type: 'POST', 324 | data: data, 325 | success: function (response) { 326 | if(response == 0){ 327 | Lobibox.notify('error', {msg: 'Delete failed, please try again', size: 'mini', sound: false}); 328 | } else if(response == 1){ 329 | item.parents('tr').css('background', '#add9ff').fadeOut('fast'); 330 | Lobibox.notify('success', {msg: 'Deleted Successfully', size: 'mini', sound: false}); 331 | } 332 | } 333 | }); 334 | }); 335 | 336 | } 337 | 338 | /** 339 | * Sends an AJAX request to delete the image. 340 | */ 341 | this.unset_image = function() { 342 | $('body').on('click', '.unset-image', function(e) { 343 | e.preventDefault(); 344 | wave_box('on'); 345 | 346 | var parent_element = $(this).parent(); 347 | 348 | $.ajax({ 349 | url: '/user/products/image/unset', 350 | type: 'POST', 351 | data: { 352 | 'action': 'unset-image', 353 | 'item_id': $('.item-edit').attr('id').split('-')[1], 354 | 'image': this.id.split('-')[1] 355 | }, 356 | success: function (response) { 357 | if(response.status == 1){ 358 | parent_element.fadeOut(); 359 | Lobibox.notify('success', {msg: response.message, size: 'mini', sound: false}); 360 | } else { 361 | Lobibox.notify('error', {msg: response.message, size: 'mini', sound: false}); 362 | } 363 | wave_box('off'); 364 | } 365 | }); 366 | }); 367 | } 368 | 369 | /** 370 | * Sends an AJAX request to set the image as featured. 371 | */ 372 | this.set_featured_image = function() { 373 | $('body').on('click', '.set-featured-image', function(e) { 374 | e.preventDefault(); 375 | wave_box('on'); 376 | 377 | var _this = this; 378 | var image_featured_id = this.id.split('-')[1]; 379 | 380 | if( $(this).hasClass('glyphicon-star') ){ 381 | Lobibox.notify('error', {msg: 'The image you clicked is already the featured image.', size: 'mini', sound: false}); 382 | wave_box('off'); 383 | 384 | } else { 385 | $.ajax({ 386 | url: '/user/products/image/set-featured', 387 | type: 'POST', 388 | data: { 389 | 'action': 'set-featured-image', 390 | 'item_id': $('.item-edit').attr('id').split('-')[1], 391 | 'image': image_featured_id 392 | }, 393 | datatype: 'JSON', 394 | success: function (response) { 395 | if(response.status == 1){ 396 | if($('.images-section').find('span.glyphicon-star').switchClass('glyphicon-star', 'glyphicon-star-empty').removeAttr('style')){ 397 | $(_this).switchClass('glyphicon-star-empty', 'glyphicon-star').css('color', '#E4C317'); 398 | Lobibox.notify('success', {msg: response.message, size: 'mini', sound: false}); 399 | } 400 | } else { 401 | Lobibox.notify('error', {msg: response.message, size: 'mini', sound: false}); 402 | } 403 | 404 | socket.emit('send', { message: { featured: image_featured_id, id: $('.item-edit').attr('id').split('-')[1] } } ); 405 | 406 | wave_box('off'); 407 | } 408 | }); 409 | } 410 | }); 411 | } 412 | 413 | /** 414 | * Load ImageViewer plugin 415 | */ 416 | this.get_imageviewer_image = function(images, curImageIdx, viewer, curSpan){ 417 | var imgObj = images[curImageIdx - 1]; 418 | 419 | viewer.load(imgObj.small, imgObj.big); 420 | curSpan.html(curImageIdx); 421 | } 422 | 423 | /** 424 | * Initialize imageviewer plugin 425 | */ 426 | this.set_imageviewer = function() { 427 | 428 | _this = this; 429 | 430 | if($('input.item-images-json').length){ 431 | var images = JSON.parse($('input.item-images-json').val()); 432 | var curImageIdx = 1, 433 | total = images.length; 434 | var wrapper = $('.imageviewer'), 435 | curSpan = wrapper.find('.current'); 436 | var viewer = ImageViewer(wrapper.find('.image-container')); 437 | 438 | /* display total count */ 439 | wrapper.find('.total').html(total); 440 | 441 | wrapper.find('.next').click(function(){ 442 | curImageIdx++; 443 | if(curImageIdx > total) curImageIdx = 1; 444 | _this.get_imageviewer_image(images, curImageIdx, viewer, curSpan); 445 | }); 446 | 447 | wrapper.find('.prev').click(function(){ 448 | curImageIdx--; 449 | if(curImageIdx < 1) curImageIdx = total; 450 | _this.get_imageviewer_image(images, curImageIdx, viewer, curSpan); 451 | }); 452 | 453 | /* initially show image */ 454 | _this.get_imageviewer_image(images, curImageIdx, viewer, curSpan); 455 | } 456 | } 457 | }, 458 | 459 | User: function() { 460 | this.init = function() { 461 | this.create_account(); 462 | this.update_account(); 463 | } 464 | 465 | this.create_account = function(){ 466 | $('.create-account').ajaxForm({ 467 | beforeSerialize: function() { 468 | wave_box('on'); 469 | }, 470 | success: function(response, textStatus, xhr, form) { 471 | if(response.status == 0){ 472 | Lobibox.notify('error', {msg: response.message, size: 'mini', sound: false}); 473 | } 474 | 475 | if(response.status == 1){ 476 | window.location.href = '/user/account'; 477 | } 478 | 479 | wave_box('off'); 480 | } 481 | }); 482 | } 483 | 484 | this.update_account = function(){ 485 | $('.update-account').ajaxForm({ 486 | beforeSerialize: function() { 487 | update_ckeditor_instances(); 488 | wave_box('on'); 489 | }, 490 | success: function(response, textStatus, xhr, form) { 491 | if(response.status == 0){ 492 | Lobibox.notify('error', {msg: response.message, size: 'mini', sound: false}); 493 | } 494 | 495 | if(response.status == 1){ 496 | Lobibox.notify('success', {msg: response.message, size: 'mini', sound: false}); 497 | } 498 | 499 | wave_box('off'); 500 | } 501 | }); 502 | } 503 | 504 | }, 505 | 506 | /** 507 | * Global 508 | */ 509 | Global: function () { 510 | 511 | /** 512 | * This method contains the list of functions that needs to be loaded 513 | * when the "Global" object is instantiated. 514 | * 515 | */ 516 | this.init = function() { 517 | this.set_ckeditor(); 518 | this.set_datepicker(); 519 | } 520 | 521 | /** 522 | * Load CKEditor plugin 523 | */ 524 | this.set_ckeditor = function() { 525 | if($('#ck-editor-area').length){ 526 | load_ckeditor('ck-editor-area', 300); 527 | } 528 | } 529 | 530 | /** 531 | * Load CKEditor plugin 532 | */ 533 | this.set_datepicker = function() { 534 | if('.datepicker'){ 535 | $('.datepicker').datetimepicker({ 536 | format: 'YYYY-MM-DD HH:mm:ss' 537 | }); 538 | } 539 | } 540 | } 541 | } 542 | 543 | /** 544 | * When the document has been loaded... 545 | * 546 | */ 547 | jQuery(document).ready( function () { 548 | 549 | global = new app.Global(); /* Instantiate the Global Class */ 550 | global.init(); /* Load Global class methods */ 551 | 552 | posts = new app.Posts(); /* Instantiate the Posts Class */ 553 | posts.init(); /* Load Posts class methods */ 554 | 555 | user = new app.User(); /* Instantiate the User Class */ 556 | user.init(); /* Load User class methods */ 557 | 558 | /* Update item data via real time */ 559 | socket.on('message', function(data) { 560 | var data = data.message; 561 | var item_id = '.item-' + data.id; 562 | for (var key in data) { 563 | if (data.hasOwnProperty(key)) { 564 | if(key == 'featured'){ 565 | $(item_id + ' .item-featured').attr('src', '/images/uploads/' + data[key]); 566 | } else if(key == 'price') { 567 | $(item_id + ' .item-price').html(parseFloat(data[key]).toFixed(2)); 568 | } else { 569 | $(item_id + ' .item-' + key).html(data[key]); 570 | } 571 | } 572 | } 573 | }); 574 | 575 | }); -------------------------------------------------------------------------------- /public/javascripts/global.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper function to get append the loading image to message container when submitting via AJAX 3 | * 4 | * @param textarea, height 5 | */ 6 | function load_ckeditor( textarea, height ) { 7 | CKEDITOR.config.allowedContent = true; 8 | CKEDITOR.replace( textarea, { 9 | toolbar: null, 10 | toolbarGroups: null, 11 | height: height 12 | }); 13 | } 14 | 15 | /** 16 | * Helper function to command CKEditor to update the instancnes before performing the AJAX call. 17 | * This will populate the hidden textfields with the proper values coming from the CKEditor 18 | * 19 | */ 20 | function update_ckeditor_instances() { 21 | for ( instance in CKEDITOR.instances ) { 22 | CKEDITOR.instances[instance].updateElement(); 23 | } 24 | } 25 | 26 | /** 27 | * Provides a nice wave animation effect 28 | * 29 | */ 30 | function wave_box_animate(){ 31 | if( $('.wave-box-effect').length ){ 32 | jQuery( ".wave-box-effect" ).css( "left", "0px" ); 33 | jQuery( ".wave-box-effect" ).animate( { 'left':"99%" }, 1000, wave_box_animate ); 34 | } 35 | } 36 | 37 | function wave_box(option) { 38 | if($('.wave-box-wrapper').length){ 39 | if(option == 'on'){ 40 | if($(".wave-box-wrapper .wave-box").html('
').show()){ 41 | wave_box_animate(); 42 | } 43 | } else if(option == 'off') { 44 | $(".wave-box-wrapper .wave-box").html('').fadeOut(); 45 | } 46 | } 47 | } 48 | 49 | /* Used for getting the parameter of a URL */ 50 | function get_url_value(variable) { 51 | var query = window.location.search.substring(1); 52 | var vars = query.split("&"); 53 | for (var i=0;i").get(0).files !== undefined; 70 | feature.formdata = window.FormData !== undefined; 71 | 72 | var hasProp = !!$.fn.prop; 73 | 74 | // attr2 uses prop when it can but checks the return type for 75 | // an expected string. this accounts for the case where a form 76 | // contains inputs with names like "action" or "method"; in those 77 | // cases "prop" returns the element 78 | $.fn.attr2 = function() { 79 | if ( ! hasProp ) { 80 | return this.attr.apply(this, arguments); 81 | } 82 | var val = this.prop.apply(this, arguments); 83 | if ( ( val && val.jquery ) || typeof val === 'string' ) { 84 | return val; 85 | } 86 | return this.attr.apply(this, arguments); 87 | }; 88 | 89 | /** 90 | * ajaxSubmit() provides a mechanism for immediately submitting 91 | * an HTML form using AJAX. 92 | */ 93 | $.fn.ajaxSubmit = function(options) { 94 | /*jshint scripturl:true */ 95 | 96 | // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) 97 | if (!this.length) { 98 | log('ajaxSubmit: skipping submit process - no element selected'); 99 | return this; 100 | } 101 | 102 | var method, action, url, $form = this; 103 | 104 | if (typeof options == 'function') { 105 | options = { success: options }; 106 | } 107 | else if ( options === undefined ) { 108 | options = {}; 109 | } 110 | 111 | method = options.type || this.attr2('method'); 112 | action = options.url || this.attr2('action'); 113 | 114 | url = (typeof action === 'string') ? $.trim(action) : ''; 115 | url = url || window.location.href || ''; 116 | if (url) { 117 | // clean url (don't include hash vaue) 118 | url = (url.match(/^([^#]+)/)||[])[1]; 119 | } 120 | 121 | options = $.extend(true, { 122 | url: url, 123 | success: $.ajaxSettings.success, 124 | type: method || $.ajaxSettings.type, 125 | iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank' 126 | }, options); 127 | 128 | // hook for manipulating the form data before it is extracted; 129 | // convenient for use with rich editors like tinyMCE or FCKEditor 130 | var veto = {}; 131 | this.trigger('form-pre-serialize', [this, options, veto]); 132 | if (veto.veto) { 133 | log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); 134 | return this; 135 | } 136 | 137 | // provide opportunity to alter form data before it is serialized 138 | if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { 139 | log('ajaxSubmit: submit aborted via beforeSerialize callback'); 140 | return this; 141 | } 142 | 143 | var traditional = options.traditional; 144 | if ( traditional === undefined ) { 145 | traditional = $.ajaxSettings.traditional; 146 | } 147 | 148 | var elements = []; 149 | var qx, a = this.formToArray(options.semantic, elements); 150 | if (options.data) { 151 | options.extraData = options.data; 152 | qx = $.param(options.data, traditional); 153 | } 154 | 155 | // give pre-submit callback an opportunity to abort the submit 156 | if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { 157 | log('ajaxSubmit: submit aborted via beforeSubmit callback'); 158 | return this; 159 | } 160 | 161 | // fire vetoable 'validate' event 162 | this.trigger('form-submit-validate', [a, this, options, veto]); 163 | if (veto.veto) { 164 | log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); 165 | return this; 166 | } 167 | 168 | var q = $.param(a, traditional); 169 | if (qx) { 170 | q = ( q ? (q + '&' + qx) : qx ); 171 | } 172 | if (options.type.toUpperCase() == 'GET') { 173 | options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; 174 | options.data = null; // data is null for 'get' 175 | } 176 | else { 177 | options.data = q; // data is the query string for 'post' 178 | } 179 | 180 | var callbacks = []; 181 | if (options.resetForm) { 182 | callbacks.push(function() { $form.resetForm(); }); 183 | } 184 | if (options.clearForm) { 185 | callbacks.push(function() { $form.clearForm(options.includeHidden); }); 186 | } 187 | 188 | // perform a load on the target only if dataType is not provided 189 | if (!options.dataType && options.target) { 190 | var oldSuccess = options.success || function(){}; 191 | callbacks.push(function(data) { 192 | var fn = options.replaceTarget ? 'replaceWith' : 'html'; 193 | $(options.target)[fn](data).each(oldSuccess, arguments); 194 | }); 195 | } 196 | else if (options.success) { 197 | callbacks.push(options.success); 198 | } 199 | 200 | options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg 201 | var context = options.context || this ; // jQuery 1.4+ supports scope context 202 | for (var i=0, max=callbacks.length; i < max; i++) { 203 | callbacks[i].apply(context, [data, status, xhr || $form, $form]); 204 | } 205 | }; 206 | 207 | if (options.error) { 208 | var oldError = options.error; 209 | options.error = function(xhr, status, error) { 210 | var context = options.context || this; 211 | oldError.apply(context, [xhr, status, error, $form]); 212 | }; 213 | } 214 | 215 | if (options.complete) { 216 | var oldComplete = options.complete; 217 | options.complete = function(xhr, status) { 218 | var context = options.context || this; 219 | oldComplete.apply(context, [xhr, status, $form]); 220 | }; 221 | } 222 | 223 | // are there files to upload? 224 | 225 | // [value] (issue #113), also see comment: 226 | // https://github.com/malsup/form/commit/588306aedba1de01388032d5f42a60159eea9228#commitcomment-2180219 227 | var fileInputs = $('input[type=file]:enabled', this).filter(function() { return $(this).val() !== ''; }); 228 | 229 | var hasFileInputs = fileInputs.length > 0; 230 | var mp = 'multipart/form-data'; 231 | var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); 232 | 233 | var fileAPI = feature.fileapi && feature.formdata; 234 | log("fileAPI :" + fileAPI); 235 | var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI; 236 | 237 | var jqxhr; 238 | 239 | // options.iframe allows user to force iframe mode 240 | // 06-NOV-09: now defaulting to iframe mode if file input is detected 241 | if (options.iframe !== false && (options.iframe || shouldUseFrame)) { 242 | // hack to fix Safari hang (thanks to Tim Molendijk for this) 243 | // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d 244 | if (options.closeKeepAlive) { 245 | $.get(options.closeKeepAlive, function() { 246 | jqxhr = fileUploadIframe(a); 247 | }); 248 | } 249 | else { 250 | jqxhr = fileUploadIframe(a); 251 | } 252 | } 253 | else if ((hasFileInputs || multipart) && fileAPI) { 254 | jqxhr = fileUploadXhr(a); 255 | } 256 | else { 257 | jqxhr = $.ajax(options); 258 | } 259 | 260 | $form.removeData('jqxhr').data('jqxhr', jqxhr); 261 | 262 | // clear element array 263 | for (var k=0; k < elements.length; k++) { 264 | elements[k] = null; 265 | } 266 | 267 | // fire 'notify' event 268 | this.trigger('form-submit-notify', [this, options]); 269 | return this; 270 | 271 | // utility fn for deep serialization 272 | function deepSerialize(extraData){ 273 | var serialized = $.param(extraData, options.traditional).split('&'); 274 | var len = serialized.length; 275 | var result = []; 276 | var i, part; 277 | for (i=0; i < len; i++) { 278 | // #252; undo param space replacement 279 | serialized[i] = serialized[i].replace(/\+/g,' '); 280 | part = serialized[i].split('='); 281 | // #278; use array instead of object storage, favoring array serializations 282 | result.push([decodeURIComponent(part[0]), decodeURIComponent(part[1])]); 283 | } 284 | return result; 285 | } 286 | 287 | // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz) 288 | function fileUploadXhr(a) { 289 | var formdata = new FormData(); 290 | 291 | for (var i=0; i < a.length; i++) { 292 | formdata.append(a[i].name, a[i].value); 293 | } 294 | 295 | if (options.extraData) { 296 | var serializedData = deepSerialize(options.extraData); 297 | for (i=0; i < serializedData.length; i++) { 298 | if (serializedData[i]) { 299 | formdata.append(serializedData[i][0], serializedData[i][1]); 300 | } 301 | } 302 | } 303 | 304 | options.data = null; 305 | 306 | var s = $.extend(true, {}, $.ajaxSettings, options, { 307 | contentType: false, 308 | processData: false, 309 | cache: false, 310 | type: method || 'POST' 311 | }); 312 | 313 | if (options.uploadProgress) { 314 | // workaround because jqXHR does not expose upload property 315 | s.xhr = function() { 316 | var xhr = $.ajaxSettings.xhr(); 317 | if (xhr.upload) { 318 | xhr.upload.addEventListener('progress', function(event) { 319 | var percent = 0; 320 | var position = event.loaded || event.position; /*event.position is deprecated*/ 321 | var total = event.total; 322 | if (event.lengthComputable) { 323 | percent = Math.ceil(position / total * 100); 324 | } 325 | options.uploadProgress(event, position, total, percent); 326 | }, false); 327 | } 328 | return xhr; 329 | }; 330 | } 331 | 332 | s.data = null; 333 | var beforeSend = s.beforeSend; 334 | s.beforeSend = function(xhr, o) { 335 | //Send FormData() provided by user 336 | if (options.formData) { 337 | o.data = options.formData; 338 | } 339 | else { 340 | o.data = formdata; 341 | } 342 | if(beforeSend) { 343 | beforeSend.call(this, xhr, o); 344 | } 345 | }; 346 | return $.ajax(s); 347 | } 348 | 349 | // private function for handling file uploads (hat tip to YAHOO!) 350 | function fileUploadIframe(a) { 351 | var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle; 352 | var deferred = $.Deferred(); 353 | 354 | // #341 355 | deferred.abort = function(status) { 356 | xhr.abort(status); 357 | }; 358 | 359 | if (a) { 360 | // ensure that every serialized input is still enabled 361 | for (i=0; i < elements.length; i++) { 362 | el = $(elements[i]); 363 | if ( hasProp ) { 364 | el.prop('disabled', false); 365 | } 366 | else { 367 | el.removeAttr('disabled'); 368 | } 369 | } 370 | } 371 | 372 | s = $.extend(true, {}, $.ajaxSettings, options); 373 | s.context = s.context || s; 374 | id = 'jqFormIO' + (new Date().getTime()); 375 | if (s.iframeTarget) { 376 | $io = $(s.iframeTarget); 377 | n = $io.attr2('name'); 378 | if (!n) { 379 | $io.attr2('name', id); 380 | } 381 | else { 382 | id = n; 383 | } 384 | } 385 | else { 386 | $io = $('