├── Procfile ├── .travis.yml ├── public ├── images │ ├── punch.png │ ├── cart-icon.png │ └── cart-icon-active.png ├── stylesheets │ ├── snipcart-style.css │ ├── reset.css │ ├── common.css │ └── style.css └── javascript │ └── functions.js ├── .gitignore ├── views ├── error_handlers │ ├── 404.pug │ └── error.pug ├── partials │ ├── footer.pug │ ├── add-to-cart-button.pug │ └── header.pug ├── listing.pug ├── product.pug └── layout.pug ├── utils └── async-handler.js ├── custom_types ├── index.json ├── category.json ├── layout.json └── product.json ├── .eslintrc.js ├── prismic-configuration.js ├── package.json ├── config.js ├── README.md └── app.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - '4' 5 | - '5' 6 | - '6' 7 | -------------------------------------------------------------------------------- /public/images/punch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prismicio/nodejs-example-snipcart/HEAD/public/images/punch.png -------------------------------------------------------------------------------- /public/images/cart-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prismicio/nodejs-example-snipcart/HEAD/public/images/cart-icon.png -------------------------------------------------------------------------------- /public/images/cart-icon-active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prismicio/nodejs-example-snipcart/HEAD/public/images/cart-icon-active.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | .idea 18 | -------------------------------------------------------------------------------- /views/error_handlers/404.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block body 4 | 5 | div.error-container 6 | h3 404 7 | h2 Sorry! 8 | h6 The page you are looking for was not found. 9 | div.back-btn 10 | a(href='/') 11 | button Go to the homepage -------------------------------------------------------------------------------- /utils/async-handler.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = (callback) => { 2 | return async (req, res, next) => { 3 | try { 4 | return await callback(req, res, next); 5 | } catch(err) { 6 | console.error(err); 7 | res.render("./error_handlers/error"); 8 | } 9 | } 10 | } 11 | 12 | module.exports = asyncHandler; -------------------------------------------------------------------------------- /views/partials/footer.pug: -------------------------------------------------------------------------------- 1 | footer 2 | div.footer-nav 3 | ul 4 | li 5 | a(href="#") About Us 6 | li 7 | a(href="#") Customer Service 8 | li 9 | a(href="#") Privacy Policy 10 | 11 | div.copywrite 12 | - var currentYear = new Date().getFullYear(); 13 | p !{"© " + currentYear + " UNKNOW"} -------------------------------------------------------------------------------- /custom_types/index.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "category", 3 | "name": "Category", 4 | "repeatable": true, 5 | "value": "category.json" 6 | }, { 7 | "id": "layout", 8 | "name": "Layout", 9 | "repeatable": false, 10 | "value": "layout.json" 11 | }, { 12 | "id": "product", 13 | "name": "Product", 14 | "repeatable": true, 15 | "value": "product.json" 16 | }] -------------------------------------------------------------------------------- /views/error_handlers/error.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block body 4 | 5 | div.error-container 6 | h4 Something went wrong. 7 | nav 8 | ul 9 | li Please check your Prismic endpoint configuration in the config/prismic-config.js file. It might be incorrect. 10 | li Please check that your Custom Type API ID is correctly defined in the queries. 11 | li Check the console for errors. -------------------------------------------------------------------------------- /custom_types/category.json: -------------------------------------------------------------------------------- 1 | { 2 | "Main" : { 3 | "uid" : { 4 | "type" : "UID", 5 | "fieldset" : "Unique ID", 6 | "config" : { 7 | "placeholder" : "Unique ID..." 8 | } 9 | }, 10 | "title" : { 11 | "type" : "StructuredText", 12 | "fieldset" : "Category Title", 13 | "config" : { 14 | "placeholder" : "Category Title...", 15 | "single" : "heading1" 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /views/listing.pug: -------------------------------------------------------------------------------- 1 | extends ./layout.pug 2 | 3 | block body 4 | 5 | div.product-list.container 6 | for product in products 7 | div.product-list-item 8 | a(href=ctx.linkResolver(product)) 9 | - var imageURL = product.data.image.Listing.url 10 | - var price = product.data.price 11 | 12 | img.product-list-image(src=imageURL) 13 | h3.product-list-title 14 | != PrismicDOM.RichText.asText(product.data.title) 15 | p.product-list-price 16 | != price ? "$" + price : '' -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": "eslint:recommended", 6 | "rules": { 7 | "no-console": 0, 8 | "indent": [ 9 | "warn", 10 | 2 11 | ], 12 | "no-unused-vars": ["error", { "vars": "all", "args": "none" }], 13 | "linebreak-style": [ 14 | "error", 15 | "unix" 16 | ], 17 | "quotes": [ 18 | "warn", 19 | "single" 20 | ], 21 | "semi": [ 22 | "error", 23 | "always" 24 | ] 25 | }, 26 | "parserOptions": { 27 | "sourceType": "module" 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /views/partials/add-to-cart-button.pug: -------------------------------------------------------------------------------- 1 | - var itemID = productContent.uid 2 | - var itemName = productContent.data.title != "" ? PrismicDOM.RichText.asText(productContent.data.title) : "Unknown" 3 | - var itemImageURL = productContent.data.image.Snipcart.url 4 | - var itemWeight = productContent.data.snipcartWeight 5 | - var itemDescription = productContent.data.snipcartDescription 6 | - var itemMaxQuantity = productContent.data.snipcartMaxQuantity 7 | - var itemURL = pageUrl 8 | 9 | a.button.add-to-cart.snipcart-add-item(href="#" data-item-id=itemID, data-item-name=itemName, data-item-price=price, data-item-image=itemImageURL, data-item-weight=itemWeight, data-item-url=itemURL, data-item-description=itemDescription, data-item-max-quantity=itemMaxQuantity) Add To Cart -------------------------------------------------------------------------------- /prismic-configuration.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | apiEndpoint: 'https://your-repo-name.prismic.io/api/v2', 4 | 5 | // -- Access token if the Master is not open 6 | // accessToken: 'xxxxxx', 7 | 8 | // OAuth 9 | // clientId: 'xxxxxx', 10 | // clientSecret: 'xxxxxx', 11 | 12 | snipcartKey: 'your-snipcart-api-key', 13 | 14 | // -- Links resolution rules 15 | // This function will be used to generate links to Prismic.io documents 16 | // As your project grows, you should update this function according to your routes 17 | linkResolver: function (doc) { 18 | if (doc.type == 'category') { 19 | return '/category/' + doc.uid; 20 | } 21 | if (doc.type == 'product') { 22 | return '/product/' + doc.uid; 23 | } 24 | return '/'; 25 | } 26 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript-nodejs-starter", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node app.js", 7 | "lint": "eslint app.js config.js prismic-configuration.js", 8 | "test": "npm run lint" 9 | }, 10 | "dependencies": { 11 | "@prismicio/client": "^4.0.0", 12 | "body-parser": "~1.18.2", 13 | "debug": "~3.1.0", 14 | "errorhandler": "~1.5.0", 15 | "express": "~4.16.1", 16 | "method-override": "~2.3.10", 17 | "morgan": "~1.9.0", 18 | "prismic-dom": "^2.2.4", 19 | "pug": "^2.0.0-alpha6", 20 | "serve-favicon": "~2.4.5" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "http://github.com/prismicio/javascript-nodejs-starter.git" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^4.8.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | var express = require('express'), 6 | favicon = require('serve-favicon'), 7 | logger = require('morgan'), 8 | bodyParser = require('body-parser'), 9 | methodOverride = require('method-override'), 10 | errorHandler = require('errorhandler'), 11 | path = require('path'); 12 | 13 | module.exports = function() { 14 | var app = express(); 15 | 16 | // all environments 17 | app.set('port', process.env.PORT || 3000); 18 | app.set('views', path.join(__dirname, 'views')); 19 | app.set('view engine', 'pug'); 20 | app.use(favicon('public/images/punch.png')); 21 | app.use(logger('dev')); 22 | app.use(bodyParser.urlencoded({ extended: true })); 23 | app.use(bodyParser.json()); 24 | app.use(methodOverride()); 25 | app.use(express.static(path.join(__dirname, 'public'))); 26 | 27 | app.use(errorHandler()); 28 | 29 | return app; 30 | }(); 31 | -------------------------------------------------------------------------------- /custom_types/layout.json: -------------------------------------------------------------------------------- 1 | { 2 | "Main" : { 3 | "title" : { 4 | "type" : "StructuredText", 5 | "config" : { 6 | "placeholder" : "Document title...", 7 | "single" : "heading1" 8 | } 9 | }, 10 | "logo" : { 11 | "type" : "Image", 12 | "fieldset" : "Site Logo" 13 | }, 14 | "mainNav" : { 15 | "type" : "Group", 16 | "fieldset" : "Main Navigation", 17 | "config" : { 18 | "fields" : { 19 | "label" : { 20 | "type" : "Text", 21 | "config" : { 22 | "placeholder" : "Link label" 23 | } 24 | }, 25 | "link" : { 26 | "type" : "Link", 27 | "config" : { 28 | "select" : "document", 29 | "customtypes" : [ "category" ], 30 | "placeholder" : "Link to a category" 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /public/stylesheets/snipcart-style.css: -------------------------------------------------------------------------------- 1 | /* Snipcart pop-up styles */ 2 | .snip-layout { 3 | font-family: Montserrat, sans-serif; 4 | } 5 | .snip-layout .snip-header { 6 | background-color: #F7F7F7; 7 | } 8 | .snip-layout .snip-header__title { 9 | color: #4FD37E; 10 | } 11 | .snip-layout .snip-header__total { 12 | background-color: transparent; 13 | color: #2F2F2F; 14 | } 15 | .snip-layout .snip-ico.snip-ico--close { 16 | color: #000000; 17 | } 18 | .snip-layout .snip-loader--bar { 19 | background-color: #F7F7F7; 20 | } 21 | .snip-layout .snip-loader--bar::before { 22 | background-color: #4FD37E; 23 | } 24 | .snip-layout .snip-btn--highlight { 25 | background-color: #4FD37E; 26 | color: #FFFFFF; 27 | } 28 | .snip-layout .snip-btn--highlight:hover { 29 | background-color: #4FD37E; 30 | } 31 | .snip-layout .snip-flash .snip-flash__item { 32 | color: #FFFFFF; 33 | border-bottom: none; 34 | } 35 | .snip-layout .custom-snipcart-footer-text { 36 | text-align: center; 37 | margin: 10px; 38 | } 39 | .snip-layout .snip-footer { 40 | background-color: #F7F7F7; 41 | } -------------------------------------------------------------------------------- /views/partials/header.pug: -------------------------------------------------------------------------------- 1 | header 2 | - var logo = layoutContent ? layoutContent.data.logo.url : null 3 | a(href="/") 4 | if logo 5 | img.logo(src=logo) 6 | else 7 | span.logo-text SiteLogo 8 | 9 | div.utility-nav 10 | ul 11 | li 12 | a(href="#") Help 13 | |  |  14 | li 15 | a(href="#") Contact Us 16 | |  |  17 | li 18 | a(href="#" class="snipcart-checkout") 19 | div.shopping-cart 20 | div.snipcart-summary.cart-count 21 | span.snipcart-total-items.cart-count-number 22 | 23 | nav.main-nav 24 | div.added-to-cart 25 | if productContent 26 | p 27 | != productContent.data.title != "" ? PrismicDOM.RichText.asText(productContent.data.title) : "This product" 28 | | has been added to your cart! 29 | - var mainNav = layoutContent ? layoutContent.data.mainNav : [] 30 | ul 31 | li 32 | a(href="/") Everything 33 | each mainLink in mainNav 34 | - var mainURL = PrismicDOM.Link.url(mainLink.link, ctx.linkResolver) 35 | li 36 | a(href=mainURL) 37 | != mainLink.label -------------------------------------------------------------------------------- /views/product.pug: -------------------------------------------------------------------------------- 1 | extends ./layout.pug 2 | 3 | block body 4 | div.container.product-details 5 | - var imageURL = productContent.data.image.url 6 | - var price = productContent.data.price 7 | 8 | div.product-image 9 | img(src=imageURL) 10 | div.product-info 11 | != PrismicDOM.RichText.asHtml(productContent.data.title) 12 | p.product-price 13 | != price ? "$" + price : '' 14 | div.product-description 15 | != PrismicDOM.RichText.asHtml(productContent.data.description) 16 | 17 | include ./partials/add-to-cart-button.pug 18 | 19 | if relatedProducts.results_size > 0 20 | div.container.recommended 21 | h2 Recommended 22 | div.recommended-wrapper 23 | for relatedProduct in relatedProducts.results 24 | - var relatedImageURL = relatedProduct.data.image.Listing.url 25 | - var relatedPrice = relatedProduct.data.price 26 | 27 | div.recommended-product 28 | a(href=ctx.linkResolver(relatedProduct)) 29 | img.recommended-image(src=relatedImageURL) 30 | p.recommended-title 31 | != PrismicDOM.RichText.asText(relatedProduct.data.title) 32 | p.recommended-price 33 | != relatedPrice ? "$" + relatedPrice : '' -------------------------------------------------------------------------------- /public/stylesheets/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html(lang='en') 2 | 3 | head 4 | title Sample eCommerse Site 5 | link(rel="stylesheet", href="/stylesheets/reset.css") 6 | link(rel="stylesheet", href="/stylesheets/common.css") 7 | link(rel="stylesheet", href="/stylesheets/style.css") 8 | link(href='https://fonts.googleapis.com/css?family=Montserrat:400,700', rel='stylesheet') 9 | link(href='https://fonts.googleapis.com/css?family=Lato:400,700', rel='stylesheet') 10 | meta(name="viewport", content="width=device-width, initial-scale=1") 11 | script(src="//code.jquery.com/jquery-2.1.1.min.js") 12 | script#snipcart(src='https://cdn.snipcart.com/scripts/2.0/snipcart.js', data-api-key=ctx.snipcartKey, data-autopop="false") 13 | link(rel='stylesheet', href='https://cdn.snipcart.com/themes/2.0/base/snipcart.min.css', type='text/css') 14 | link(rel="stylesheet", href="/stylesheets/snipcart-style.css") 15 | script(src="/javascript/functions.js") 16 | 17 | // Required for Previews and Experiments 18 | script 19 | = ctx?"window.prismic = { endpoint: '" + ctx.endpoint + "' };":"" 20 | script(src="//static.cdn.prismic.io/prismic.min.js") 21 | 22 | 23 | body 24 | 25 | include ./partials/header.pug 26 | 27 | block body 28 | 29 | include ./partials/footer.pug 30 | 31 | script#cart-content-text(type='text/template'). 32 |
35 | -------------------------------------------------------------------------------- /public/stylesheets/common.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-font-smoothing: antialiased; 3 | } 4 | ::selection { 5 | background: #FFF7C7; /* WebKit/Blink Browsers */ 6 | } 7 | ::-moz-selection { 8 | background: #FFF7C7; /* Gecko Browsers */ 9 | } 10 | 11 | /* 12 | * Globals 13 | */ 14 | body { 15 | padding: 0; 16 | color: #72767b; 17 | font-family: Montserrat, sans-serif; 18 | font-size: 16px; 19 | font-weight: 400; 20 | letter-spacing : 0.4; 21 | line-height: 28px; 22 | } 23 | a { 24 | color: #72767B; 25 | font-size: 14px; 26 | font-weight: 400; 27 | letter-spacing : 0.35; 28 | line-height: 28px; 29 | text-decoration: none; 30 | } 31 | p a { 32 | text-decoration: underline; 33 | } 34 | h1, h2, h3, h4, h5, h6 { 35 | font-family: Montserrat, sans-serif; 36 | } 37 | h1, h1 a { 38 | font-size: 26px; 39 | font-weight: bold; 40 | color: #2F2F2F; 41 | line-height: 32px; 42 | letter-spacing : 0.5; 43 | margin-bottom: 1rem; 44 | } 45 | h2, h2 a { 46 | font-size: 13px; 47 | font-weight: normal; 48 | color: #AFAFAF; 49 | line-height: 13px; 50 | letter-spacing : 0.3; 51 | text-transform: uppercase; 52 | margin-bottom: 1rem; 53 | } 54 | h3, h3 a { 55 | font-size: 20px; 56 | font-weight: normal; 57 | color: #2F2F2F; 58 | line-height: 30px; 59 | letter-spacing : 0.5; 60 | margin-bottom: 1rem; 61 | } 62 | p { 63 | margin-bottom: 2rem; 64 | } 65 | pre, ul { 66 | margin-bottom: 20px; 67 | } 68 | strong { 69 | font-weight: bold; 70 | } 71 | em { 72 | font-style: italic; 73 | } 74 | img { 75 | max-width: 100%; 76 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Sample eCommerce Website with API-based CMS 2 | 3 | This is a sample Node.js eCommerce website with content managed with prismic.io (API-based CMS). 4 | 5 | #### Getting started 6 | 7 | Read [this guide](https://intercom.help/prismicio/examples/nodejs-samples/sample-ecommerce-site-with-snipcart-in-nodejs) for instructions to create your repository and use this sample eCommerce site. 8 | 9 | #### Deploy your Node.js website 10 | 11 | An easy way to deploy your Node.js website is to use [Heroku](http://www.heroku.com). Just follow these few simple steps once you have successfully [signed up](https://id.heroku.com/signup/www-header) and [installed the Heroku toolbelt](https://toolbelt.heroku.com/): 12 | 13 | Create a new Heroku application 14 | 15 | ``` 16 | $ heroku create 17 | ``` 18 | 19 | Initialize a new Git repository: 20 | 21 | ``` 22 | $ git init 23 | $ heroku git:remote -a your-heroku-app-name 24 | ``` 25 | 26 | Commit your code to the Git repository and deploy it to Heroku: 27 | 28 | ``` 29 | $ git add . 30 | $ git commit -am "make it better" 31 | $ git push heroku master 32 | ``` 33 | 34 | Ensure you have at least one node running: 35 | 36 | ``` 37 | $ heroku ps:scale web=1 38 | ``` 39 | 40 | You can now browse your application online: 41 | 42 | ``` 43 | $ heroku open 44 | ``` 45 | 46 | ### Licence 47 | 48 | This software is licensed under the Apache 2 license, quoted below. 49 | 50 | Copyright 2017 Prismic.io (http://www.prismic.io). 51 | 52 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. 53 | 54 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /public/javascript/functions.js: -------------------------------------------------------------------------------- 1 | // On document load 2 | $( document ).ready(function() { 3 | 4 | // Function to remove the cart count and reset the cart color 5 | function defaultCart() { 6 | $('.shopping-cart').css('background-image', 'url("../images/cart-icon.png")'); 7 | $('.cart-count').hide(); 8 | } 9 | 10 | // Function to show the cart count and change the color of the cart to green 11 | function highlightedCart() { 12 | $('.cart-count').show(); 13 | $('.shopping-cart').css('background-image', 'url("../images/cart-icon-active.png")'); 14 | } 15 | 16 | // Display custom text in the snipcart shopping cart 17 | Snipcart.execute('bind', 'cart.opened', function() { 18 | Snipcart.execute('unbind', 'cart.opened'); 19 | 20 | var html = $("#cart-content-text").html(); 21 | $(html).insertBefore($("#snipcart-footer")); 22 | }); 23 | 24 | // If there is nothing in the cart on page load, don't display a number 25 | Snipcart.subscribe('cart.ready', function (data) { 26 | var cartCount = data.order ? data.order.items.length : 0; 27 | if (cartCount > 0) { 28 | $('.cart-count').show(); 29 | $('.shopping-cart').css('background-image', 'url("../images/cart-icon-active.png")'); 30 | } else { 31 | $('.shopping-cart').css('background-image', 'url("../images/cart-icon.png")'); 32 | } 33 | }); 34 | 35 | // If an item is added to the cart, set to highlighted cart 36 | Snipcart.subscribe('item.added', function (ev, item, items) { 37 | highlightedCart(); 38 | $("html, body").animate({ scrollTop: 0 }, "slow"); 39 | $('.added-to-cart').stop(true, false).slideDown('slow').delay(2000).slideUp('slow'); 40 | return false; 41 | }); 42 | 43 | // If all items are removed, set to default cart 44 | Snipcart.subscribe('item.removed', function (ev, item, items) { 45 | var cartCount = Snipcart.api.items.count(); 46 | if (cartCount == 0) { 47 | defaultCart() 48 | } 49 | }); 50 | 51 | // If an order is completed, set to default cart 52 | Snipcart.subscribe('order.completed', function (data) { 53 | defaultCart() 54 | }); 55 | }); -------------------------------------------------------------------------------- /custom_types/product.json: -------------------------------------------------------------------------------- 1 | { 2 | "Main" : { 3 | "uid" : { 4 | "type" : "UID", 5 | "fieldset" : "Unique Identifier", 6 | "config" : { 7 | "placeholder" : "UID" 8 | } 9 | }, 10 | "date" : { 11 | "type" : "Date", 12 | "fieldset" : "Date Added" 13 | }, 14 | "title" : { 15 | "type" : "StructuredText", 16 | "fieldset" : "Product Title", 17 | "config" : { 18 | "single" : "heading1", 19 | "placeholder" : "Title..." 20 | } 21 | }, 22 | "categories" : { 23 | "type" : "Group", 24 | "fieldset" : "Categories", 25 | "config" : { 26 | "fields" : { 27 | "link" : { 28 | "type" : "Link", 29 | "config" : { 30 | "select" : "document", 31 | "customtypes" : [ "category" ], 32 | "placeholder" : "Link to a category" 33 | } 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | "Product Info" : { 40 | "image" : { 41 | "type" : "Image", 42 | "fieldset" : "Product Image", 43 | "config" : { 44 | "constraint" : { 45 | "width" : 640 46 | }, 47 | "thumbnails" : [ { 48 | "name" : "Listing", 49 | "width" : 300, 50 | "height" : 300 51 | }, { 52 | "name" : "Snipcart", 53 | "width" : 50, 54 | "height" : 50 55 | } ] 56 | } 57 | }, 58 | "price" : { 59 | "type" : "Number", 60 | "fieldset" : "Price", 61 | "config" : { 62 | "label" : "USD", 63 | "min" : 1, 64 | "placeholder" : "Insert Price..." 65 | } 66 | }, 67 | "description" : { 68 | "type" : "StructuredText", 69 | "fieldset" : "Product Description", 70 | "config" : { 71 | "multi" : "paragraph,em,strong,hyperlink", 72 | "placeholder" : "Description..." 73 | } 74 | } 75 | }, 76 | "Related Products" : { 77 | "relatedProducts" : { 78 | "type" : "Group", 79 | "fieldset" : "Link to related products", 80 | "config" : { 81 | "fields" : { 82 | "link" : { 83 | "type" : "Link", 84 | "config" : { 85 | "select" : "document", 86 | "customtypes" : [ "product" ], 87 | "placeholder" : "Link to a product" 88 | } 89 | } 90 | } 91 | } 92 | } 93 | }, 94 | "Snipcart Data" : { 95 | "snipcartDescription" : { 96 | "type" : "Text", 97 | "fieldset" : "Snipcart Data", 98 | "config" : { 99 | "label" : "Brief Description", 100 | "placeholder" : "Brief Description..." 101 | } 102 | }, 103 | "snipcartWeight" : { 104 | "type" : "Number", 105 | "config" : { 106 | "label" : "Weight (g)", 107 | "min" : 1 108 | } 109 | }, 110 | "snipcartMaxQuantity" : { 111 | "type" : "Number", 112 | "config" : { 113 | "label" : "Max Quantity", 114 | "min" : 1 115 | } 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | const Prismic = require('@prismicio/client'); 7 | const PrismicDOM = require('prismic-dom'); 8 | const app = require('./config'); 9 | const PrismicConfig = require('./prismic-configuration'); 10 | const asyncHandler = require ("./utils/async-handler"); 11 | const PORT = app.get('port'); 12 | 13 | function render404(req, res) { 14 | res.status(404); 15 | res.render("./error_handlers/404"); 16 | } 17 | 18 | app.listen(PORT, () => { 19 | process.stdout.write(`Point your browser to: http://localhost:${PORT}\n`); 20 | }); 21 | 22 | // Middleware to inject prismic context 23 | app.use((req, res, next) => { 24 | res.locals.ctx = { 25 | endpoint: PrismicConfig.apiEndpoint, 26 | snipcartKey: PrismicConfig.snipcartKey, 27 | linkResolver: PrismicConfig.linkResolver 28 | }; 29 | // add PrismicDOM in locals to access them in templates. 30 | res.locals.PrismicDOM = PrismicDOM; 31 | Prismic.getApi(PrismicConfig.apiEndpoint,{ accessToken: PrismicConfig.accessToken, req: req }) 32 | .then((api) => { 33 | req.prismic = { api }; 34 | next(); 35 | }).catch(function(err) { 36 | if (err.status == 404) { 37 | res.status(404).send('There was a problem connecting to your API, please check your configuration file for errors.'); 38 | } else { 39 | res.status(500).send('Error 500: ' + err.message); 40 | } 41 | }); 42 | }); 43 | 44 | 45 | // Query the site layout with every route 46 | app.route('*').get((req, res, next) => { 47 | req.prismic.api.getSingle('layout').then(function(layoutContent){ 48 | 49 | // Give an error if no layout custom type is found 50 | if (!layoutContent) { 51 | res.status(500).send('No Layout document was found.'); 52 | } 53 | 54 | // Define the layout content 55 | res.locals.layoutContent = layoutContent; 56 | next(); 57 | }); 58 | }); 59 | 60 | 61 | /* 62 | * -------------- Routes -------------- 63 | */ 64 | 65 | /* 66 | * Preconfigured prismic preview 67 | */ 68 | 69 | // Prismic preview route 70 | app.get('/preview', asyncHandler(async (req, res, next) => { 71 | const { token, documentId } = req.query; 72 | if(token){ 73 | try{ 74 | const redirectUrl = (await req.prismic.api.getPreviewResolver(token, documentId).resolve(PrismicConfig.linkResolver, '/')); 75 | res.redirect(302, redirectUrl); 76 | }catch(e){ 77 | res.status(500).send(`Error 500 in preview`); 78 | } 79 | }else{ 80 | res.send(400, 'Missing token from querystring'); 81 | } 82 | next(); 83 | })) 84 | 85 | /* 86 | * Route for the product pages 87 | */ 88 | app.route('/product/:uid').get(function(req, res) { 89 | 90 | // Get the page url needed for snipcart 91 | var pageUrl = req.protocol + '://' + req.get('host') + req.originalUrl; 92 | 93 | // Define the UID from the url 94 | var uid = req.params.uid; 95 | 96 | // Query the product by its UID 97 | req.prismic.api.getByUID('product', uid).then(function(productContent) { 98 | 99 | // Render the 404 page if this uid is found 100 | if (!productContent) { 101 | render404(req, res); 102 | } 103 | 104 | // Collect all the related product IDs for this product 105 | var relatedProducts = productContent.data.relatedProducts; 106 | var relatedIDs = relatedProducts.map((relatedProduct) => { 107 | var link = relatedProduct.link; 108 | return link ? link.id : null; 109 | }).filter((id) => id !== null && id !== undefined); 110 | 111 | //Query the related products by their IDs 112 | req.prismic.api.getByIDs(relatedIDs).then(function(relatedProducts) { 113 | 114 | // Render the product page 115 | res.render('product', { 116 | productContent: productContent, 117 | relatedProducts: relatedProducts, 118 | pageUrl: pageUrl 119 | }); 120 | }); 121 | }); 122 | }); 123 | 124 | 125 | // Route for categories 126 | app.route('/category/:uid').get(function(req, res) { 127 | 128 | // Define the UID from the url 129 | var uid = req.params.uid; 130 | 131 | // Query the category by its UID 132 | req.prismic.api.getByUID('category', uid).then(function(category) { 133 | 134 | // Render the 404 page if this uid is found 135 | if (!category) { 136 | render404(req, res); 137 | } 138 | 139 | // Define the category ID 140 | var categoryID = category.id; 141 | 142 | // Query all the products linked to the given category ID 143 | req.prismic.api.query([ 144 | Prismic.Predicates.at('document.type', 'product'), 145 | Prismic.Predicates.at('my.product.categories.link', categoryID) 146 | ], { orderings : '[my.product.date desc]'} 147 | ).then(function(products) { 148 | 149 | // Render the listing page 150 | res.render('listing', {products: products.results}); 151 | }); 152 | }); 153 | }); 154 | 155 | 156 | // Route for the homepage 157 | app.route('/').get(function(req, res) { 158 | 159 | // Query all the products and order by their dates 160 | req.prismic.api.query( 161 | Prismic.Predicates.at('document.type', 'product'), 162 | { orderings : '[my.product.date desc]'} 163 | ).then(function(products) { 164 | 165 | // Render the listing page 166 | res.render('listing', {products: products.results}); 167 | }); 168 | }); 169 | 170 | 171 | // Route that catches any other url and renders the 404 page 172 | app.route('/:url').get(function(req, res) { 173 | render404(req, res); 174 | }); 175 | 176 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | /* General */ 2 | .container, header, footer { 3 | max-width: 980px; 4 | margin: auto; 5 | padding: 0 20px; 6 | } 7 | .button { 8 | display: inline-block; 9 | padding: 20px 60px; 10 | font-size: 16px; 11 | line-height: 16px; 12 | font-weight: normal; 13 | letter-spacing: 0.4; 14 | color: #ffffff; 15 | background: #4FD37E; 16 | text-transform: uppercase; 17 | border-radius: 4px; 18 | } 19 | 20 | /* Header */ 21 | header { 22 | display: flex; 23 | justify-content: space-between; 24 | align-items: center; 25 | padding: 40px 20px; 26 | } 27 | .logo { 28 | max-height: 40px; 29 | max-width: 200px; 30 | } 31 | .logo-text { 32 | display: block; 33 | font-size: 20px; 34 | font-weight: bold; 35 | color: #2F2F2F; 36 | line-height: 20px; 37 | letter-spacing : 0.5; 38 | } 39 | .utility-nav ul { 40 | list-style: none; 41 | margin: 0; 42 | } 43 | .utility-nav ul li { 44 | display: inline-block; 45 | vertical-align: middle; 46 | } 47 | .utility-nav ul, 48 | .utility-nav a { 49 | font-size: 16px; 50 | font-weight: regular; 51 | color: #AFAFAF; 52 | line-height: 16px; 53 | letter-spacing : 0.4; 54 | } 55 | .snipcart-checkout { 56 | display: inline-flex; 57 | align-items: center; 58 | position: relative; 59 | } 60 | .shopping-cart { 61 | width: 20px; 62 | height: 20px; 63 | margin-right: 20px; 64 | margin-left: 5px; 65 | background: #FFFFFF center center no-repeat; 66 | background-size: 100%; 67 | } 68 | .cart-count { 69 | display: none; 70 | position: absolute; 71 | left: 28px; 72 | color: #4FD37E; 73 | } 74 | 75 | 76 | /* Main Navigation */ 77 | .main-nav { 78 | position: relative; 79 | width: 100%; 80 | height: 60px; 81 | margin-bottom: 40px; 82 | text-align: center; 83 | background: #F7F7F7; 84 | } 85 | .main-nav ul { 86 | list-style: none; 87 | margin: 0; 88 | } 89 | .main-nav ul li { 90 | display: inline-block; 91 | margin: 0 10px; 92 | } 93 | .main-nav ul, 94 | .main-nav a { 95 | font-size: 16px; 96 | font-weight: regular; 97 | color: #2F2F2F; 98 | line-height: 60px; 99 | letter-spacing : 0.4; 100 | } 101 | .main-nav a:hover { 102 | color: #000000; 103 | } 104 | .added-to-cart { 105 | display: none; 106 | position: absolute; 107 | width: 100%; 108 | height: 100%; 109 | background: #4FD37E; 110 | text-align: center; 111 | } 112 | .added-to-cart p { 113 | margin: 0; 114 | padding: 0; 115 | font-size: 16px; 116 | line-height: 60px; 117 | letter-spacing : 0.4; 118 | color: #FFFFFF; 119 | } 120 | 121 | /* Product Listing */ 122 | .product-list { 123 | display: flex; 124 | flex-wrap: wrap; 125 | justify-content: center; 126 | } 127 | .product-list-item { 128 | width: 300px; 129 | margin-right: 20px; 130 | margin-bottom: 20px; 131 | text-align: center; 132 | } 133 | .product-list-item:nth-child(3n) { 134 | margin-right: 0; 135 | } 136 | .product-list-image { 137 | margin-bottom: 20px; 138 | } 139 | .product-list-title { 140 | line-height: 20px; 141 | margin-bottom: 20px; 142 | } 143 | .product-list-price { 144 | font-size: 20px; 145 | font-weight: bold; 146 | color: #2F2F2F; 147 | line-height: 20px; 148 | letter-spacing : 0.5; 149 | } 150 | 151 | 152 | /* Product Details */ 153 | .product-details { 154 | display: flex; 155 | justify-content: space-between; 156 | padding-bottom: 80px; 157 | } 158 | .product-image { 159 | flex: 1; 160 | margin-right: 20px; 161 | } 162 | .product-info { 163 | flex: 1; 164 | } 165 | .product-info h1 { 166 | margin-bottom: 10px; 167 | } 168 | .product-price { 169 | font-size: 22px; 170 | font-weight: normal; 171 | color: #2F2F2F; 172 | line-height: 28px; 173 | letter-spacing : 0.5; 174 | margin-bottom: 30px; 175 | } 176 | .product-description { 177 | margin-bottom: 40px; 178 | text-align: left; 179 | font-family: Lato, sans-serif; 180 | font-size: 17px; 181 | letter-spacing: 0.2; 182 | color: #2F2F2F; 183 | } 184 | .recommended { 185 | padding-top: 20px; 186 | border-top: 3px solid #F7F7F7; 187 | } 188 | .recommended-wrapper { 189 | display: flex; 190 | flex-wrap: wrap; 191 | justify-content: center; 192 | } 193 | .recommended-product { 194 | margin-right: 20px; 195 | margin-bottom: 20px; 196 | text-align: center; 197 | } 198 | .recommended-product:nth-child(3n) { 199 | margin-right: 0; 200 | } 201 | .recommended-image { 202 | margin-bottom: 10px; 203 | } 204 | .recommended-title { 205 | margin-bottom: 20px; 206 | font-size: 16px; 207 | font-weight: regular; 208 | color: #2F2F2F; 209 | line-height: 16px; 210 | letter-spacing : 0.4; 211 | text-align: center; 212 | } 213 | .recommended-price { 214 | margin-bottom: 20px; 215 | font-size: 18px; 216 | font-weight: bold; 217 | color: #2F2F2F; 218 | line-height: 18px; 219 | letter-spacing : 0.5; 220 | text-align: center; 221 | } 222 | 223 | 224 | /* Footer */ 225 | footer { 226 | display: flex; 227 | justify-content: space-between; 228 | align-items: center; 229 | margin-top: 40px; 230 | padding: 40px 20px; 231 | border-top: 3px solid #F7F7F7; 232 | } 233 | .footer-nav ul { 234 | list-style: none; 235 | margin: 0; 236 | } 237 | .footer-nav ul li { 238 | display: inline-block; 239 | margin: 0 20px 0 0; 240 | } 241 | footer p, 242 | footer a { 243 | margin: 0; 244 | font-size: 13px; 245 | font-weight: normal; 246 | color: #AFAFAF; 247 | line-height: 13px; 248 | letter-spacing : 0.3; 249 | text-transform: uppercase; 250 | } 251 | footer a { 252 | text-decoration: none; 253 | } 254 | 255 | /* Error handler & 404 pages */ 256 | .error-container { 257 | max-width: 600px; 258 | margin: auto; 259 | margin-top: 230px; 260 | margin-bottom: 449px; 261 | } 262 | .error-container h3 { 263 | text-align: center; 264 | font-size: 60px; 265 | } 266 | .error-container h2, 267 | .error-container h6 { 268 | font-size: 20px; 269 | text-align: center; 270 | } 271 | .back-btn { 272 | text-align: center; 273 | } 274 | .error-container button { 275 | padding: 20px 30px; 276 | margin-top: 20px; 277 | border-radius: 20px; 278 | font-size: 20px; 279 | } 280 | .error-container nav ul { 281 | list-style: circle; 282 | } 283 | .error-container nav ul li { 284 | margin-bottom: 20px; 285 | } 286 | 287 | /* Media Queries */ 288 | @media (max-width: 400px) { 289 | .main-nav ul, .main-nav ul a { 290 | font-size: 12px; 291 | } 292 | } 293 | @media (max-width: 500px) { 294 | header { 295 | display: block; 296 | text-align: center; 297 | } 298 | .logo, 299 | .logo-text { 300 | margin-bottom: 20px; 301 | } 302 | } 303 | @media (max-width: 659px) { 304 | .product-list-item, 305 | .product-list-item:nth-child(2n), 306 | .product-list-item:nth-child(3n), 307 | .recommended-product, 308 | .recommended-product:nth-child(2n), 309 | .recommended-product:nth-child(3n) { 310 | margin-right: 20px; 311 | margin-left: 20px; 312 | } 313 | } 314 | @media (max-width: 639px) { 315 | .product-list-item, 316 | .product-list-item:nth-child(2n), 317 | .product-list-item:nth-child(3n), 318 | .recommended-product, 319 | .recommended-product:nth-child(2n), 320 | .recommended-product:nth-child(3n) { 321 | margin-right: 0px; 322 | margin-left: 0px; 323 | } 324 | } 325 | @media (max-width: 750px) { 326 | .product-details, 327 | .recommended-wrapper { 328 | display: block; 329 | text-align: center; 330 | } 331 | .product-image { 332 | margin-right: 0; 333 | } 334 | .recommended-image { 335 | max-width: 300px; 336 | width: 100%; 337 | } 338 | footer { 339 | display: block; 340 | text-align: center; 341 | } 342 | .footer-nav ul { 343 | display: block; 344 | margin-bottom: 20px; 345 | } 346 | .footer-nav ul li { 347 | display: block; 348 | margin: 0 0 10px; 349 | } 350 | } 351 | @media (min-width: 660px) and (max-width: 979px) { 352 | .product-list-item:nth-child(3n), 353 | .recommended-product:nth-child(3n) { 354 | margin-right: 20px; 355 | } 356 | .product-list-item:nth-child(2n), 357 | .recommended-product:nth-child(2n) { 358 | margin-right: 0; 359 | } 360 | .error-container { 361 | padding: 23px 58px; 362 | } 363 | } --------------------------------------------------------------------------------