+ <%= product.title %> +
+$ + <%= product.price %> +
++ <%= product.description %> +
+diff --git a/app.js b/app.js index d6a0724..f24e2ee 100644 --- a/app.js +++ b/app.js @@ -1,30 +1,87 @@ -const path = require('path'); +const path = require('path'); const express = require('express'); const bodyParser = require('body-parser'); -const adminData = require('./routes/admin'); +const adminRoutes = require('./routes/admin'); const shopRoutes = require('./routes/shop'); const rootDir = require('./util/path'); +const errorController = require('./controller/error'); +const sequelize = require('./util/database'); + +// requiring both the models +const Product = require('./model/product'); +const User = require('./model/user'); const app = express(); -// no need to register the engin, as exoress auto register it app.set('view engine', 'ejs'); app.set('views', 'views'); -app.use(bodyParser.urlencoded({extended : true})); +app.use(bodyParser.urlencoded({ extended: true })); app.use(express.static(path.join(rootDir, 'public'))); -app.use('/admin',adminData.routes); +// registering the new middleware, because we want to store that user in our request so that we can use it from anywhere in our app +app.use((req, res, next) => { + // now we want to reachout to our database and retrive the user + // with the help of + // User.findByPk(1); + + // IMPORTANT: + // you might think that will this ever return a user if are craeting down in sync() method + // app only registered the middleware function. so for incoming request, it will then execute this middleware function + // npm start will first run sequelize.sync() and then user ill get registed if it not there, then server runs + // once the server start running, request will be made and this function will get executed + + User.findByPk(1) + .then((user) => { + // we want to storethe user in request + req.user = user; + // we can do that, we are just adding the user field to "req" object + // we just have to amke sure, we dont overwrite the exisiting user + // "req.user = user;" here user is undefined by default, so now we are storing user which we retrived from the database + // user which are reterving is not just a JS object with values stored in db and .Its a sequelize object with values stored in the database and with all the utility methods sequelize added like destroy. + // thus, we are storing sequelize object in request object, not just JS object + // hence whenver we user now, we can operate sequelize methods on it. + + // we need to call "next()" so taht we can continue with the next step if we get our user and store it. + next(); + }) + .catch(err => console.log(err)); +}); + +app.use('/admin', adminRoutes); app.use(shopRoutes); -app.use((req, res, next) => { - res.status(404); - res.setHeader('Content-type','text/html'); - // passing the argument to templating engine doesnt change, it remains the same - // "path: 'Error'" we passed becasue ejs engin will check "path" argument in naviagation bar logic of template. if we will not provide, we will get an error - res.render('404', {pageTitle: "404", path: 'Error'}); -}); - -app.listen(3000); \ No newline at end of file +app.use(errorController.get404); + +Product.belongsTo(User, {constraints: true, onDelete: 'CASCADE'}); +User.hasMany(Product); + +// sequelize.sync({force: true}) +sequelize.sync() +.then(result => { + // craeting a dummy user, which is logged in as sooon as server starts. + return User.findByPk(1) +}) +.then( user => { + if(!user) { + // here are passing promise + return User.create({name: 'max', email: 'test@t.com'}); + } + // but here only object will be returned. + // return user; + // we should always return the same so that next "then()" can be executed + // so, returning user promise + // return Promise.resolve(user); + // but no need of that as whatever is returned from "then()" is wrapped inside promise itself. hence + return user; +}) +.then(user => { + console.log(user); + // hence, user is registered successfully. and we can start the server + app.listen(3000); +}) +.catch(err => { + console.log('err', err); +}); \ No newline at end of file diff --git a/controller/admin.js b/controller/admin.js new file mode 100644 index 0000000..7cf0500 --- /dev/null +++ b/controller/admin.js @@ -0,0 +1,99 @@ +const Product = require("../model/product"); + +exports.getAddProduct = (req, res, next) => { + res.render("admin/edit-product", { + path: "/admin/add-product", + pageTitle: "Add Products", + editing: false + }); +}; + +exports.postAddProduct = (req, res, next) => { + const title = req.body.title; + const imageUrl = req.body.imageUrl; + const description = req.body.description; + const price = req.body.price; + + Product.create({ + title: title, + imageUrl: imageUrl, + price: price, + description: description + }) + .then(result => { + // console.log(result); + console.log('product saved '); + res.redirect('/admin/products'); + }) + .catch(err => { + console.log(err); + }); + +}; + +exports.getEditProduct = (req, res, next) => { + const editMode = req.query.edit + if(!editMode) { + res.redirect('/'); + } + const productID = req.params.productID; + + Product.findByPk(productID) + .then(product => { + res.render("admin/edit-product", { + path: "/admin/edit-product", + pageTitle: "Edit Products", + editing: editMode, + product: product + }); + }) + .catch(err => console.log(err)); +}; + +exports.postEditProduct = (req, res, next) => { + const productID = req.body.productID; + const updatedTitle = req.body.title; + const updatedImageUrl = req.body.imageUrl; + const updatedPrice = req.body.price; + const upadatedDesciption = req.body.description; + + Product.findByPk(productID) + .then(product => { + product.title = updatedTitle; + product.imageUrl = updatedImageUrl; + product.price = updatedPrice; + product.description = upadatedDesciption; + + product.save() + }) + .then(() => { + res.redirect('/admin/products'); + }) + .catch(err => console.log(err)); +}; + +exports.postDeleteProduct = (req, res, next) => { + const productID = req.body.productID; + Product.findByPk(productID) + .then(product => { + product.destroy() + }) + .then(() => { + res.redirect('/admin/products'); + }) + .catch(err => console.log(err)); +}; + +exports.getProducts = (req, res, next) => { + Product.findAll() + .then(products => { + res.render("admin/products", { + path: "/admin/products", + pageTitle: "Admin Add Products", + prods: products, + }); + }) + .catch(err => { + console.log(err); + }); +}; \ No newline at end of file diff --git a/controller/error.js b/controller/error.js new file mode 100644 index 0000000..5d3d895 --- /dev/null +++ b/controller/error.js @@ -0,0 +1,5 @@ +exports.get404 = (req, res, next) => { + res.status(404); + res.setHeader('Content-type', 'text/html'); + res.render('404', { pageTitle: "404", path: 'Error' }); +}; \ No newline at end of file diff --git a/controller/shop.js b/controller/shop.js new file mode 100644 index 0000000..ba17f1a --- /dev/null +++ b/controller/shop.js @@ -0,0 +1,108 @@ +const Product = require('../model/product'); +const Cart = require('../model/cart'); + +exports.getProduct = (req, res, next) => { + Product.findAll() + .then(products => { + res.render("shop/product-list", { + pageTitle: "Shop Products", + prods: products, + path: "/products" + }); + }) + .catch(err => console.log(err)); +}; + +exports.getDetails = (req, res, next) => { + const productID = req.params.productId; + + // Product.findByPk(productID) + // .then(product => { + // console.log(product); + // res.render( + // 'shop/product-detail', + // { pageTitle: product.title, + // product: product, + // path: '/products' + // } + // ); + // }) + // .catch(err => console.log(err)); + + // or + + Product.findAll({where : + {id: productID} + }) + .then(product => { + res.render( + 'shop/product-detail', + { + pageTitle: product[0].title, + product: product[0], + path: '/products' + } + ); + }) + .catch(err => console.log(err)); +}; + +exports.getCart = (req, res, next) => { + Cart.getCart(cart => { + Product.fetchAll(products => { + const cartProducts = []; + for(product of products) { + const CartproductData = cart.products.find(prod => prod.id === product.id) + if(CartproductData) { + cartProducts.push({productData: product, qty: CartproductData.qty}); + } + } + res.render('shop/cart', { + pageTitle: 'Cart', + path: '/cart', + products: cartProducts + }); + }); + }); +}; + +exports.postCart = (req, res, next) => { + const productID = req.body.productID + console.log(productID); + Product.findById(productID, product => { + Cart.addProduct(productID, product.price); + }) + res.redirect('/'); +} + +exports.postCartDeleteProduct = (req, res, next) => { + const productID = req.body.productID; + Product.findById(productID, product => { + Cart.deleteProduct(productID, product.price); + res.redirect('/cart'); + }); +}; + +exports.getOrders = (req, res, next) => { + res.render('shop/orders', { + pageTitle: 'Orders', + path: '/orders' + }); +}; + +exports.getIndex = (req, res, next) => { + Product.findAll() + .then(products => { + res.render("shop/index", { + pageTitle: "Shop", + prods: products, + path: "/" + }); + }) + .catch(err => console.log(err)); +}; + + + + + diff --git a/data/cart.json b/data/cart.json new file mode 100644 index 0000000..794ca83 --- /dev/null +++ b/data/cart.json @@ -0,0 +1 @@ +{"products":[],"totalPrice":null} \ No newline at end of file diff --git a/data/products.json b/data/products.json new file mode 100644 index 0000000..6f49935 --- /dev/null +++ b/data/products.json @@ -0,0 +1 @@ +[{"id":"0.21217683817981658","title":"book","imageUrl":" data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBw8QDxIPDxAQEA8PDQ0PDw8PDw8PEBAPFREWFhUVFRUYHSggGBolHRUVITEhJSkrLi4uFx8zODMtNygtLisBCgoKDg0OGhAQGC8iHSUtLSsrLS0rLS0rMC4rLTAtLS0rLSstLS0tLS0rLS0tLzUtLS0tLSstLS0tLS0tLS0rK//AABEIALcBEwMBIgACEQEDEQH/xAAbAAACAgMBAAAAAAAAAAAAAAADBAACAQUGB//EAEkQAAIBAgIFBgoGCAYBBQAAAAECAAMRBCEFEjFBUQYTYXGBkQciMlKSobHB0fBCVHKT0uEWFyNDYoKj8RREU2Oy4lUVM0Vzg//EABkBAQEBAQEBAAAAAAAAAAAAAAABAgMEBf/EACgRAQACAQQBBAIBBQAAAAAAAAABAhEDEyFREjFBUmEUkQQiMkKBof/aAAwDAQACEQMRAD8A8vUQirIohVWBFWECyKsKqwMKsIqyyrCKsCqrCqksqQirAqqy4WXVYRUgUCS4SEVIQJAEElgkKElwkAISZCQ4SWCQF9SZ1IxqSakBbUk1IzqSakBXUmCkZ1JgpAVKShSNlJQpAUKShWNlIMpAUKyjLGmSDZYCrJBssaZYNlgKssEyxplg2WAqywTLGmWCZYC+rJCFZmBFWEVZFWFVYEVYVVmVWFVYGFWFVZlVhVWBhVhVSWRIVVgVVIRUl1SEVIFFSXCQqpCKkAQSXFOGCS4SAAU5YJGAksEgLakzqRnm5nm4CupMaka5uTm4ChSVKRwpKlICRpyhSOlINkgJMkoyRxkg2SAmywTJHGSCZICbLBsscZIFkgKMsEyxtlgmWAoywTLGmWCZYC1pIUrJAirCqswohkWBlFhVWRVhkWBFWGRZEWGVYGFWGVZlVhkSBVUhlSWVIZUgUVIRUnUaD0ErUi9UZ1B4o4Lxmv0loh6LcU3N8YyNWqQgpwqpCBIAAksKcOElwk","description":"1","price":"11"}] \ No newline at end of file diff --git a/model/cart.js b/model/cart.js new file mode 100644 index 0000000..6a69aaf --- /dev/null +++ b/model/cart.js @@ -0,0 +1,72 @@ +const fs = require('fs'); +const path = require('path'); + +const rootDir = require('../util/path'); + +const p = path.join(rootDir, 'data', 'cart.json'); + +module.exports = class Cart { + static addProduct(id, productPrice) { + // fetch the previous product + fs.readFile(p, (err, fileContent) => { + let cart = { products: [], totalPrice: 0 }; + if (!err) { + cart = JSON.parse(fileContent); + } + const exisitingProductIndex = cart.products.findIndex(prod => prod.id === id); + const exisitingProduct = cart.products[exisitingProductIndex]; + let updatedProduct; + + // add new product / incraese the quantity + if (exisitingProduct) { + updatedProduct = { ...exisitingProduct }; + updatedProduct.qty = updatedProduct.qty + 1; + cart.products = [...cart.products]; + cart.products[exisitingProductIndex] = updatedProduct; + } else { + updatedProduct = { id: id, qty: 1 }; + // updating the cart after adding new product to the cart + cart.products = [...cart.products, updatedProduct]; + } + cart.totalPrice = cart.totalPrice + +productPrice; + fs.writeFile(p, JSON.stringify(cart), err => { + console.log(err); + }); + }); + } + + static deleteProduct(id, productPrice) { + fs.readFile(p, (err, fileContent) => { + if (err) { + return; + } + const updatedProduct = { ...JSON.parse(fileContent) }; + const product = updatedProduct.products.find(prods => prods.id === id); + // if we delete a product from admin page and that product is not inn the cart, then it will give us error. + // SOLUTION:- to delete, we first need to check if the given product is in cart, if it is not there then we simply need to return . we dont want to try to edit it as it is not there. + if(!product) { + return; + } + const productQty = product.qty; + updatedProduct.products = updatedProduct.products.filter(prods => prods.id !== id); + updatedProduct.totalPrice = updatedProduct - (productPrice * productQty); + + fs.writeFile(p, JSON.stringify(updatedProduct), (err) => { + console.log(err); + }); + }); + } + + static getCart(cb) { + fs.readFile(p, (err, fileContent) => { + const cart = JSON.parse(fileContent); + if(err) { + cb(null); + } else { + cb(cart); + } + }); + } + +} + diff --git a/model/product.js b/model/product.js new file mode 100644 index 0000000..ac1956f --- /dev/null +++ b/model/product.js @@ -0,0 +1,27 @@ +const Sequelize = require('sequelize'); + +const sequelize = require('../util/database'); + +const Product = sequelize.define('product', { + id : { + type: Sequelize.INTEGER, + allowNull : false, + autoIncrement : true, + primaryKey : true + }, + title : Sequelize.STRING, + price : { + type: Sequelize.DOUBLE, + allowNull: false + }, + imageUrl : { + type: Sequelize.STRING, + allowNull: false + }, + description : { + type : Sequelize.STRING, + allowNull: false + } +}); + +module.exports = Product; diff --git a/model/user.js b/model/user.js new file mode 100644 index 0000000..387ad53 --- /dev/null +++ b/model/user.js @@ -0,0 +1,25 @@ +// There will be one dummy user who can add items in cart and later can checkout. + +const Sequelize = require('sequelize'); + +const sequelize = require('../util/database'); + +// table name will be in single quotes '' +const User = sequelize.define('user', { + id : { + primaryKey: true, + allowNull: false, + autoIncrement: true, + type: Sequelize.INTEGER + }, + name : { + type: Sequelize.STRING, + allowNull: false + }, + email : { + type: Sequelize.STRING, + allowNull: false + } +}); + +module.exports = User; \ No newline at end of file diff --git a/notes.txt b/notes.txt index d5bb9e7..f1003a0 100644 --- a/notes.txt +++ b/notes.txt @@ -1,38 +1,13 @@ -express js is all about middleware -ie, incoming request will passthrough bunch of functions -thus, there will be more than request handler. - it helps in code segreation +11.158 +app.js +11.159 +app.js -we can have same url for more then one webpage till the time method ccessing that webpages are different -eg: /admin/add-products => GET - /admin/add-products => POST - routes.get('/add-products', (req, res, next) => { - routes.post('/add-products', (req, res, next) => { -res.sendFile('/views/addProducts.html'); -this will cause error, as node will check for a file from root directory of and not from -root directory of the project -thus, "path" module will be used to specify the path to html files. - -this will not work as node will not allow us to acces us the internal files on the browser -ie, localhost:300/public/css/main.css - -to access these static files, we have to modify our main app.js file -we need to serve files statically and stically simply means that its not handles by -router or expressjs or any other middleware. -but should be directly forwarded to file sysytem - - -installing templating engines -npm install --save ejs pug express-handlebars - - -pug: -html syntax is different but if we inspect page, it will shows us proper html code \ No newline at end of file diff --git a/package.json b/package.json index 03b305b..72d3c19 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "ejs": "^3.0.1", "express": "^4.17.1", "express-handlebars": "^3.1.0", - "pug": "^2.0.4" + "mysql2": "^2.1.0", + "pug": "^2.0.4", + "sequelize": "^5.21.3" } } diff --git a/public/css/forms.css b/public/css/forms.css index 81d1cdc..8284ed0 100644 --- a/public/css/forms.css +++ b/public/css/forms.css @@ -3,18 +3,21 @@ } .form-control label, -.form-control input { +.form-control input, +.form-control textarea { display: block; width: 100%; margin-bottom: 0.25rem; } -.form-control input { +.form-control input, +.form-control textarea { border: 1px solid #a1a1a1; font: inherit; border-radius: 2px; } -.form-control input:focus { +.form-control input:focus, +.form-control textarea:focus{ outline-color: #00695c; } diff --git a/public/css/main.css b/public/css/main.css index 77286bb..2ffca14 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -15,16 +15,22 @@ main { margin: auto; } +form { + display: inline; +} + .main-header { width: 100%; height: 3.5rem; background-color: #00695c; padding: 0 1.5rem; + display: flex; + align-items: center; } .main-header__nav { height: 100%; - display: flex; + display: none; align-items: center; } @@ -51,6 +57,83 @@ main { color: #ffeb3b; } +.mobile-nav { + width: 30rem; + height: 100vh; + max-width: 90%; + position: fixed; + left: 0; + top: 0; + background: white; + z-index: 10; + padding: 2rem 1rem 1rem 2rem; + transform: translateX(-100%); + transition: transform 0.3s ease-out; +} + +.mobile-nav.open { + transform: translateX(0); +} + +.mobile-nav__item-list { + list-style: none; + display: flex; + flex-direction: column; + margin: 0; + padding: 0; +} + +.mobile-nav__item { + margin: 1rem; + padding: 0; +} + +.mobile-nav__item a { + text-decoration: none; + color: black; + font-size: 1.5rem; + padding: 0.5rem 2rem; +} + +.mobile-nav__item a:active, +.mobile-nav__item a:hover, +.mobile-nav__item a.active { + background: #00695c; + color: white; + border-radius: 3px; +} + +#side-menu-toggle { + border: 1px solid white; + font: inherit; + padding: 0.5rem; + display: block; + background: transparent; + color: white; + cursor: pointer; +} + +#side-menu-toggle:focus { + outline: none; +} + +#side-menu-toggle:active, +#side-menu-toggle:hover { + color: #ffeb3b; + border-color: #ffeb3b; +} + +.backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + background: rgba(0, 0, 0, 0.5); + z-index: 5; + display: none; +} + .grid { display: flex; flex-wrap: wrap; @@ -83,16 +166,19 @@ main { } .card__actions { - padding: 1rem; - text-align: center; + padding: 1rem; + text-align: center; } .card__actions button, .card__actions a { - margin: 0 0.25rem; + margin: 0 0.25rem; } .btn { + display: inline-block; + padding: 0.25rem 1rem; + text-decoration: none; font: inherit; border: 1px solid #00695c; color: #00695c; @@ -106,3 +192,25 @@ main { background-color: #00695c; color: white; } + +.centered { + text-align: center; +} + +.image { + height: 20rem; +} + +.image img { + height: 100%; +} + +@media (min-width: 768px) { + .main-header__nav { + display: flex; + } + + #side-menu-toggle { + display: none; + } +} diff --git a/public/css/product.css b/public/css/product.css index 2fe37ae..2ce1a08 100644 --- a/public/css/product.css +++ b/public/css/product.css @@ -2,11 +2,13 @@ width: 20rem; max-width: 90%; margin: auto; + display: block; } .product-item { width: 20rem; max-width: 95%; + margin: 1rem; } .product__title { diff --git a/public/js/main.js b/public/js/main.js new file mode 100644 index 0000000..3d56207 --- /dev/null +++ b/public/js/main.js @@ -0,0 +1,16 @@ +const backdrop = document.querySelector('.backdrop'); +const sideDrawer = document.querySelector('.mobile-nav'); +const menuToggle = document.querySelector('#side-menu-toggle'); + +function backdropClickHandler() { + backdrop.style.display = 'none'; + sideDrawer.classList.remove('open'); +} + +function menuToggleClickHandler() { + backdrop.style.display = 'block'; + sideDrawer.classList.add('open'); +} + +backdrop.addEventListener('click', backdropClickHandler); +menuToggle.addEventListener('click', menuToggleClickHandler); diff --git a/routes/admin.js b/routes/admin.js index 259e70e..480508d 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -1,22 +1,20 @@ - const path = require('path'); const express = require('express'); -const rootDir = require('../util/path'); +const adminController = require('../controller/admin'); const routes = express.Router(); -const products = []; -routes.get('/add-product', (req, res, next) => { - res.render('addProduct', {path: "admin/add-product", pageTitle : 'Add Products', activeAddProduct: true, formsCSS: true, productCSS: true}) -}); +routes.get('/add-product', adminController.getAddProduct); + +routes.post('/add-product', adminController.postAddProduct); + +routes.get('/edit-product/:productID', adminController.getEditProduct); + +routes.post('/edit-product', adminController.postEditProduct); -routes.post('/add-product', (req, res, next) => { - products.push({'title' : req.body.title}); - console.log('admin.js ', products); - res.redirect('/'); -}); +routes.post('/delete-product', adminController.postDeleteProduct); -module.exports.routes = routes; -module.exports.productArr = products; \ No newline at end of file +routes.get('/products',adminController.getProducts) +module.exports = routes; diff --git a/routes/shop.js b/routes/shop.js index a558eea..c6dd55f 100644 --- a/routes/shop.js +++ b/routes/shop.js @@ -2,18 +2,22 @@ const path = require('path'); const express = require('express'); -const adminData = require('./admin'); +const shopController = require('../controller/shop'); const routes = express.Router(); -routes.get('/', (req, res, next) => { - // "layout: false" is speacial keyword which is understood by handlerbars which tells us that we are not using anhy layouts. thus no error regarding layouts will pop up. - // res.render('shop', {pageTitle: 'Shop', prods : adminData.productArr, path : '/', hasProducts : adminData.productArr.length > 0, layout: false}); +routes.get('/', shopController.getIndex); - res.render('shop', {pageTitle: 'Shop', prods : adminData.productArr, path : '/', hasProducts : adminData.productArr.length > 0, activeShop: true, productCSS: true}); +routes.get('/products', shopController.getProduct); - console.log('shop.js ', adminData.productArr); -}); +routes.get('/products/:productId', shopController.getDetails); -module.exports = routes; +routes.get('/cart', shopController.getCart); + +routes.post('/cart', shopController.postCart); + +routes.post('/cart-delete-item', shopController.postCartDeleteProduct); +routes.get('/orders', shopController.getOrders); + +module.exports = routes; diff --git a/util/database.js b/util/database.js new file mode 100644 index 0000000..142f774 --- /dev/null +++ b/util/database.js @@ -0,0 +1,15 @@ +// proper and nomal way +// const Sequelize = require('sequelize'); + +// this is buggy, using to get intellisense working +const Sequelize = require('sequelize').Sequelize; + +const sequelize = new Sequelize('nodeapp', 'root', 'root', { + dialect: 'mysql', + host: 'localhost' +}); + +module.exports = sequelize; + + + diff --git a/util/notes.txt b/util/notes.txt deleted file mode 100644 index 48340ca..0000000 --- a/util/notes.txt +++ /dev/null @@ -1,4 +0,0 @@ -util folder is nothing but a helper folder -it means that it contains a helper file ie, it will help other files - -path.js file is helping other js files in setting up path more easilt and efficently diff --git a/util/path.js b/util/path.js index f6cff62..df0c281 100644 --- a/util/path.js +++ b/util/path.js @@ -1,14 +1,3 @@ const path = require('path'); -// "dirname" : Return the directory name of a path -// so, we have to find out for which dirfile, we have to get the name -// "process" : its a global variable, present in all the files -// it contains the mainModule property -// "mainModule" : tells the main module which started our application -// "filename": to get the filename where main module file started the application - -// "(process.mainModule.filename)" it gives us the name of main application which started all the process ie "app.js" - -// dirname(process.mainModule.filename) : it will gives the path of "app.js" file where it is located. - -module.exports = path.dirname(process.mainModule.filename) \ No newline at end of file +module.exports = path.dirname(process.mainModule.filename); \ No newline at end of file diff --git a/views/404.ejs b/views/404.ejs index 171cd1d..c258e04 100644 --- a/views/404.ejs +++ b/views/404.ejs @@ -1,7 +1,8 @@ -<%- include('./includes/head.ejs') %> +<%- include('includes/head.ejs') %> + -<%- include('./includes/navigation.ejs') %> +
+ <%- include('includes/navigation.ejs') %> ++ <%= product.description %> +
+