<repo_name>morsssss/amp-ecomm<file_sep>/src/server/server.js
const express = require('express');
const request = require('request');
const path = require('path');
const fs = require('fs');
const productApiManager = require('./ApiManager.js');
const mustache = require("mustache");
const formidableMiddleware = require('express-formidable');
const sessions = require("client-sessions");
const serializer = require('serialize-to-js');

const app = express();

app.use(formidableMiddleware());
app.use(sessions({
    cookieName: 'session',
    secret: 'eommercedemoofamazigness',
    duration: 24 * 60 * 60 * 1000,
    activeDuration: 1000 * 60 * 5
}));

app.engine('html', function(filePath, options, callback) {
    fs.readFile(filePath, function(err, content) {
        if (err)
            return callback(err)
        var rendered = mustache.to_html(content.toString(), options);
        return callback(null, rendered)
    });
});
app.set('view engine', 'html');
app.set('views', __dirname + '/../');

const apiManager = new productApiManager();

const port = process.env.PORT || 8080;
const listener = app.listen(port, () => {
    console.log('App listening on port ' + listener.address().port);
});

//serve static files
app.use(express.static(path.join(__dirname, '/../')));

//Product Listing Page
app.get('/product-listing', function(req, res) {
    // defaults to women shirts
    let resProductsGender = 'women';
    let resProductsCategory = 'shirts';
    let resShirtSelected = true;
    let resShortSelected = false;

    let productsGender = req.query.gender || resProductsGender;
    let productsCategory = req.query.category || resProductsCategory;
    let listingUrl = apiManager.getCategoryUrl(productsGender + '-' + productsCategory);

    if (!listingUrl.match('categoryId=undefined')) {
        resProductsCategory = productsCategory;
        resProductsGender = productsGender;
        if (!resProductsGender.match('women')) {
            resProductsGender = 'men';
        }
        if (!resProductsCategory.match('shirt')) {
            resShirtSelected = false;
            resShortSelected = true;
        }
    }
    mustache.tags = ['<%', '%>'];
    let responseObj = {
        productsCategory: resProductsCategory,
        productsGender: resProductsGender
    };

    if (resShirtSelected) {
        responseObj.shirtSelected = true;
    } else if (resShortSelected) {
        responseObj.shortSelected = true;
    }

    if (resProductsGender == 'women') {
        responseObj.womenSelected = true;
    }

    res.render('product-listing', responseObj);
});

//Product Page
app.get('/product-details', function(req, res) {

    let categoryId = req.query.categoryId;
    let productId = req.query.productId;
    let productUrl = apiManager.getProductUrl(productId);

    const options = {
        url: productUrl
    };

    request(options, (error, response, body) => {
        if (!error && body != 'Product not found' && !body.includes('An error has occurred')) {
            var productObj = apiManager.parseProduct(body);
            productObj.CategoryId = categoryId;
            mustache.tags = ['<%', '%>'];
            res.render('product-details', productObj);
        } else {
            res.render('product-not-found');
        }
    });
});

app.get('/shopping-cart', function(req, res) {

    //get related products for the cart page: items belonging to the category of the first item in the cart, excluding the item.
    let shoppingCart = req.session.shoppingCart;
    let relatedProductsObj = new Object();

    if (shoppingCart) {
        shoppingCart = serializer.deserialize(shoppingCart);
        let cartItems = shoppingCart.cartItems;
        if(cartItems.length > 0) {
            let firstCartItem = cartItems[0];
            relatedProductsObj.Main_Id = firstCartItem.productId;
            relatedProductsObj.CategoryId = firstCartItem.categoryId;
        }
    }

    mustache.tags = ['<%', '%>'];
    res.render('cart-details', relatedProductsObj);
});

//API
app.post('/api/add-to-cart', function(req, res) {

    let productId = req.fields.productId;
    let categoryId = req.fields.categoryId;
    let name = req.fields.name;
    let price = req.fields.price;
    let color = req.fields.color;
    let size = req.fields.size;
    let imgUrl = req.fields.imgUrl;
    let origin = req.get('origin');
    let quantity = req.fields.quantity;

    //If comes from the cache
    if (req.headers['amp-same-origin'] !== 'true') {
        //transfrom POST into GET and redirect to same url
        let queryString = 'productId=' + productId + '&categoryId=' + categoryId + '&name=' + name + '&price=' + price + '&color=' + color + '&size=' + size + '&quantity=' + quantity + '&origin=' + origin + '&imgUrl=' + imgUrl;
        res.header("AMP-Redirect-To", origin + "/api/add-to-cart?" + queryString);
    } else {
        updateShoppingCartOnSession(req, productId, categoryId, name, price, color, size, imgUrl, quantity);
        res.header("AMP-Redirect-To", origin + "/shopping-cart");
    }

    enableCors(req, res);
    //amp-form requires json response
    res.json({});
});

app.get('/api/add-to-cart', function(req, res) {
    let productId = req.query.productId;
    let categoryId = req.query.categoryId;
    let name = req.query.name;
    let price = req.query.price;
    let color = req.query.color;
    let size = req.query.size;
    let imgUrl = req.query.imgUrl;
    let quantity = req.query.quantity;

    updateShoppingCartOnSession(req, productId, categoryId, name, price, color, size, imgUrl, quantity);
    res.redirect('/shopping-cart');
});

app.get('/api/categories', function(req, res) {

    let categoryId = req.query.categoryId;
    let sort = req.query.sort;

    let categoryUrl = apiManager.getCategoryUrl(categoryId, sort);
    console.log("Calling Category Url: " + categoryUrl);

    const options = {
        url: categoryUrl
    };

    request(options, (error, response, body) => {
        if (!error) {
            res.send(apiManager.parseCategory(body));
        } else {
            res.json({ error: 'An error occurred in /api/categories' });
        }
    });
});

app.get('/api/product', function(req, res) {

    let productId = req.query.productId;
    let productUrl = apiManager.getProductUrl(productId);

    const options = {
        url: productUrl
    };

    request(options, (error, response, body) => {
        if (!error && body != 'Product not found' && !body.includes('An error has occurred')) {
            var productObj = apiManager.parseProduct(body);
            res.send(productObj);
        } else {
            res.json({ error: 'An error occurred in /api/product: ' + body });
        }
    });
});

// Wrap the shopping cart into an 'items' array, so it can be consumed with amp-list.
app.get('/api/cart-items', function(req, res) {
    let cart = getCartFromSession(req);

    let response = { items: [cart] };

    enableCors(req, res);
    res.send(response);
});

app.get('/api/cart-count', function(req, res) {
    let cart = getCartFromSession(req);

    let response = { items: [cart.cartItems.length] };

    enableCors(req, res);
    res.send(response);
});

app.post('/api/delete-cart-item', function(req, res) {

    let productId = req.fields.productId;
    let color = req.fields.color;
    let size = req.fields.size;

    let shoppingCartResponse = { items: [] };

    let shoppingCart = req.session.shoppingCart;

    if (shoppingCart) {
        shoppingCart = serializer.deserialize(shoppingCart);
        shoppingCart.removeItem(productId, color, size);
        req.session.shoppingCart = serializer.serialize(shoppingCart);
        shoppingCartResponse.items.push(shoppingCart);
    }

    enableCors(req, res);
    res.send(shoppingCartResponse);
});

app.get('/api/related-products', function(req, res) {
    let categoryId = req.query.categoryId;
    let productId = req.query.productId;

    let categoryUrl = apiManager.getCategoryUrl(categoryId);

    //the response will be a 1 element items array, to be able to combine amp-list with amp-mustache
    //see: https://github.com/ampproject/amphtml/issues/4405#issuecomment-379696849
    let relatedProductsResponse = { items: [] };

    const options = {
        url: categoryUrl
    };

    request(options, (error, response, body) => {
        if (!error) {
            let relatedProducts = apiManager.getRelatedProducts(productId, body);
            relatedProductsResponse.items.push(relatedProducts);
            res.send(relatedProductsResponse);
        } else {
            res.json({ error: 'An error occurred in /api/related-products' });
            console.log(error);
        }
    });
});

// If the session contains a cart, then deserialize it and return that!
// Otherwise, create a new cart and add that to the session.
function getCartFromSession(req) {
    let sessionCart = req.session.shoppingCart;

    if (sessionCart) {
        return serializer.deserialize(sessionCart);
    } else {
        let newCart = apiManager.createCart();
        req.session.shoppingCart = serializer.serialize(newCart);
        return cart;
    }
}

function updateShoppingCartOnSession(req, productId, categoryId, name, price, color, size, imgUrl, quantity) {
    let cartProduct = apiManager.createCartItem(productId, categoryId, name, price, color, size, imgUrl, quantity);
    let shoppingCartJson = req.session.shoppingCart;
    let shoppingCartObj;

    if (shoppingCartJson) {
        shoppingCartObj = serializer.deserialize(shoppingCartJson);
    } else {
        shoppingCartObj = apiManager.createCart();
    }

    shoppingCartObj.addItem(cartProduct);
    req.session.shoppingCart = serializer.serialize(shoppingCartObj);
}

function enableCors(req, res) {

    //set to all for dev purposes only, change it by configuration to final domain
    let sourceOrigin = req.query.__amp_source_origin;
    let origin = req.get('origin') || sourceOrigin;

    res.header('Access-Control-Allow-Credentials', 'true');
    res.header("Access-Control-Allow-Origin", origin);
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    res.header("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin,AMP-Redirect-To");
    res.header("AMP-Access-Control-Allow-Source-Origin", sourceOrigin);
}<file_sep>/gulpfile.js
/* Dependencies */
const gulp = require('gulp');
const plumber = require('gulp-plumber');
const autoprefixer = require('gulp-autoprefixer');
const fileinclude = require('gulp-file-include');
const sass = require('gulp-sass');
const filter = require('gulp-filter')
const minimist = require('minimist');
const del = require('del');
const gulpAmpValidator = require('gulp-amphtml-validator');
const bs = require('browser-sync').create();
const autoScript = require('amphtml-autoscript').create();
const reload = bs.reload;
const nodemon = require('gulp-nodemon');
const replace = require('gulp-replace');
const noop = require('gulp-noop');
const mergeMediaQuery = require('gulp-merge-media-queries');

// Build type is configurable such that some options can be changed e.g. whether
// to minimise CSS. Usage 'gulp <task> --env development'.
const knownOptions = {
    string: 'env',
    default: { env: process.env.NODE_ENV || 'dist' }
};

const options = minimist(process.argv.slice(2), knownOptions);

const paths = {
    css: {
        src: 'src/sass/**/*.scss',
        dest: 'src/css/'
    },
    html: {
        src: 'src/html/pages/*.html',
        dest: 'dist/'
    },
    images: {
        src: 'src/img/**/*.{gif,jpg,png,svg}',
        dest: 'dist/img'
    },
    favicon: {
        src: 'src/favicon/*',
        dest: 'dist/'
    },
    rootConfig: {
        src: 'src/rootConfigFiles/*',
        dest: 'dist/'
    },
    server: {
        src: 'src/server/**/*',
        dest: 'dist/server'
    }
};

/**
 * Builds the styles, bases on SASS files taken from src. The resulting CSS is
 * used as partials that are included in the final AMP HTML.
 * When SASS sees a non-ASCII character in a file, it starts the CSS file it builds with "@charset "UTF-8";".
 * That's great in CSS files, but not accepted within <style> tags.
 * So unless the SASS team takes on https://github.com/sass/sass/issues/2288, we need to remove it.
 */

gulp.task('styles', function buildStyles() {
    const cssEncodingDirective = '@charset "UTF-8";';

    return gulp.src(paths.css.src)
        .pipe(plumber())
        .pipe(sass(options.env === 'dist' ? { outputStyle: 'compressed' } : {}))
        .pipe(options.env === 'dev' ? replace(cssEncodingDirective, '') : noop())
        .pipe(autoprefixer('last 10 versions'))
        .pipe(mergeMediaQuery({log: true}))
        .pipe(gulp.dest(paths.css.dest));
});

/**
 * Copies the images to the distribution.
 */
gulp.task('images', function buildImages() {
    return gulp.src(paths.images.src)
        .pipe(gulp.dest(paths.images.dest));
});

/**
 * Copies the favicon to the distribution.
 */
gulp.task('favicon', function buildImages() {
    return gulp.src(paths.favicon.src)
        .pipe(gulp.dest(paths.favicon.dest));
});

/**
 * Copies the root config files to the distribution.
 */
gulp.task('rootConfig', function buildImages() {
    return gulp.src(paths.rootConfig.src)
        .pipe(gulp.dest(paths.rootConfig.dest));
});


/**
 * Copies the server and helper classes to the distribution.
 */
gulp.task('server', function buildImages() {
    return gulp.src(paths.server.src)
        .pipe(gulp.dest(paths.server.dest));
});

/**
 * Builds the HTML files. Only files from 'pages' are built, such that partials
 * are ignored as targets.
 */
gulp.task('html', gulp.series('styles', function buildHtml() {
    const pageFilter = filter(['**/pages/*.html']);
    return gulp.src(paths.html.src)
        .pipe(pageFilter)
        .pipe(fileinclude({
            prefix: '%%',
            basepath: '@file'
        }))
        .pipe(autoScript())
        .pipe(gulp.dest(paths.html.dest));
}));

/**
 * Checks resulting output AMP HTML for validity.
 */
gulp.task('validate', function validate() {
    return gulp.src(paths.html.dest + '/**/*.html')
        .pipe(gulpAmpValidator.validate())
        .pipe(gulpAmpValidator.format())
        .pipe(gulpAmpValidator.failAfterError());
});

/**
 * Removes all files from the distribution directory, and also the CSS build
 * directory.
 */
gulp.task('clean', function clean() {
    return del([
        paths.html.dest + '/**/*',
        paths.css.dest + '/**/*'
    ]);
});

/**
 * Builds the output from sources.
 */
gulp.task('build', gulp.series('images', 'favicon', 'rootConfig', 'html', 'server', 'validate'));

/**
 * First rebuilds the output then triggers a reload of the browser.
 */
gulp.task('rebuild', gulp.series('build', function rebuild(done) {
    bs.reload();
    done();
}));

/**
 * Sets up the live browser sync.
 */
/* 
gulp.task('serve', function sync(done) {
    bs.init({
        server: {
            baseDir: 'dist/'
        }
    });
    done();
});
*/
gulp.task('browser-sync', function sync(done) {
    bs.init(null, {
        proxy: "http://localhost:8080", // port of node server
    });
    done();
});

gulp.task('nodemon', function (cb) {
    var callbackCalled = false;
    return nodemon({script: './dist/server/server.js'}).on('start', function () {
        if (!callbackCalled) {
            callbackCalled = true;
            cb();
        }
    });
});

/**
 * Sets up live-reloading: Changes to HTML or CSS trigger a rebuild, changes to
 * images, favicon, root config files and server only result in images, favicon, root config files, server and helper classes being copied again to dist.
 */
gulp.task('watch', function watch(done) {
    gulp.watch(paths.images.src, gulp.series('images'));
    gulp.watch(paths.favicon.src, gulp.series('favicon'));
    gulp.watch(paths.rootConfig.src, gulp.series('rootConfig'));
    gulp.watch(paths.server.src, gulp.series('server'));
    gulp.watch('src/html/**/*.html', gulp.series('rebuild'));
    gulp.watch(paths.css.src, gulp.series('rebuild'));
    done();
});

/**
 * Prepares a clean build.
 */
gulp.task('prepare', gulp.series('clean', 'build'));

/**
 * Default task is to perform a clean build then set up browser sync for live
 * reloading.
 */
gulp.task('default', gulp.series('clean', 'build', 'nodemon', 'browser-sync', 'watch'));<file_sep>/README.md
Note that the templates directory contains files that will be turned into partials and page.  It will eventually vanish or be moved.
