├── .eslintrc.js ├── .gitignore ├── .prettierrc.yaml ├── .vscode └── settings.json ├── README.md ├── package.json ├── sample.slater.config.js └── src ├── assets ├── fonts.css.liquid ├── index.js └── main.css ├── config ├── settings_data.json └── settings_schema.json ├── layout ├── gift_card.liquid ├── password.liquid └── theme.liquid ├── locales └── en.default.json ├── scripts ├── app.js ├── components │ ├── accountLogin.js │ ├── cartDrawer.js │ ├── cartDrawerItem.js │ ├── header.js │ ├── hero.js │ ├── product-counter.js │ ├── product-selection.js │ ├── product.js │ └── slater-welcome.js ├── index.js └── lib │ ├── cart.js │ ├── choozy.js │ ├── currency.js │ ├── getProductJson.js │ ├── images.js │ ├── options.js │ ├── product-selector.js │ ├── radio.js │ └── select.js ├── sections ├── footer.liquid ├── header.liquid ├── hero-collection.liquid ├── hero.liquid └── slater-welcome.liquid ├── snippets ├── account-address-form.liquid ├── account-address.liquid ├── account-titles.liquid ├── cart-drawer.liquid ├── component-button.liquid ├── component-counter.liquid ├── component-image.liquid ├── component-input.liquid ├── component-radio.liquid ├── component-select.liquid ├── component-textarea.liquid ├── css-variables.liquid ├── head-meta.liquid ├── hero.liquid ├── pagination.liquid ├── product-head.liquid ├── product-options.liquid ├── search.liquid └── social-sharing.liquid ├── styles ├── components │ ├── buttons.scss │ ├── cart-drawer.scss │ ├── counter.scss │ └── image.scss ├── global │ ├── colors.scss │ ├── containers.scss │ ├── forms.scss │ ├── global.scss │ ├── lists.scss │ ├── typography.scss │ └── var.scss ├── main.scss └── templates │ └── .gitkeep └── templates ├── 404.liquid ├── article.liquid ├── blog.liquid ├── cart.liquid ├── collection.liquid ├── customers ├── account.liquid ├── activate_account.liquid ├── addresses.liquid ├── login.liquid ├── order.liquid ├── register.liquid └── reset_password.liquid ├── gift_card.liquid ├── index.liquid ├── list-collections.liquid ├── page.contact.liquid ├── page.liquid ├── password.liquid ├── product.liquid ├── product.neue.liquid └── search.liquid /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | }, 6 | extends: ['eslint:recommended', 'prettier', 'plugin:import/warnings'], 7 | globals: { 8 | Atomics: 'readonly', 9 | SharedArrayBuffer: 'readonly', 10 | window: 'writable', 11 | document: 'writable', 12 | slater: 'readable', 13 | }, 14 | parser: 'babel-eslint', 15 | parserOptions: { 16 | ecmaVersion: 2018, 17 | sourceType: 'module', 18 | }, 19 | extends: ['eslint:recommended', 'prettier', 'plugin:import/warnings'], 20 | plugins: ['prettier', 'import'], 21 | rules: { 22 | 'class-methods-use-this': 0, 23 | 'no-unused-expressions': [2, { allowTaggedTemplates: true }], 24 | 'no-underscore-dangle': 0, 25 | 'prettier/prettier': 2, 26 | 'consistent-return': 0, 27 | 'import/no-extraneous-dependencies': 0, 28 | 'max-len': [2, 120], 29 | 'object-curly-spacing': [2, 'always'], 30 | semi: [1, 'never'], 31 | 'no-console': 0, 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | config.yml 4 | build 5 | test.config.js 6 | *.zip 7 | slater.config.js -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | tabWidth: 4 2 | semi: false 3 | singleQuote: true 4 | arrowParens: 'always' 5 | trailingComma: 'es5' 6 | printWidth: 120 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/build": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slater Base 2 | 3 | ## What is this? 4 | 5 | This is a base template based heavily on [Slater](https://github.com/the-couch/slater), an excellent Shopify development toolkit inspired by [Slate](https://github.com/Shopify/slate). 6 | 7 | ## Getting started 8 | 9 | ### Install dependencies 10 | 11 | ``` 12 | npm i 13 | ``` 14 | 15 | ### Edit configuration file 16 | 17 | 1. **Duplicate** `sample.slater.config.js` 18 | 2. **Rename** to `slater.config.js` 19 | 3. Change the `id`, `password` and `store` values to point to your Shopify environment 20 | 21 | ### Allow https locally 22 | 23 | In order for livereload to work properly, you have to allow https to run locally on port `4000`. To do so: 24 | 25 | - Run `npm run start` on the command line and then navigate to https://localhost:4000. 26 | - Then authorize the browser to view page despite the lack of an SSL certificate. 27 | - You should see the message "Slater running" on the subsequent screen. 28 | 29 | ### Setup your IDE 30 | 31 | We recommend that you use VS Code but most modern IDE's should do the job. This template comes configured with [eslint](https://eslint.org/) and [prettier](https://prettier.io/). 32 | 33 | In order take full advantage of these plugins, you should install the Prettier extension and update your preferences to auto format on save. 34 | 35 | ## Commands 36 | 37 | ### `npm run start` 38 | 39 | Watches for changes and auto uploads changed files to Shopify. When running this, the command line should print out the preview URL. Click on this to view your changes as your develop. 40 | 41 | ### `npm run deploy:development` 42 | 43 | Builds and compiles all assets and then uploads entire theme to Shopify environment specified in the configuration file. Use this if you would like to push up changes that were made prior to running `npm run start`. 44 | 45 | ## Shopify Theme Basics 46 | 47 | I would highly recommend reading the [Shopify Theme development documentation](https://help.shopify.com/en/themes/development) before diving into any Shopify project. Having a strong understanding of this system will save you a lot of time and confusion in the long run. The following is only intended to be a brief summary: 48 | 49 | ### Layouts 50 | 51 | A special type of template that wraps the entire site. A shopify site needs to have at least one of these. The layout typically contains the header, footer and main content area. 52 | 53 | ### Templates 54 | 55 | Think of templates like pages. There are templates for product pages, blog posts, blog listing pages etc. 56 | 57 | ### Sections 58 | 59 | Sections are pieces of content that need to be editable in the Shopify CMS. There are **dynamic** sections and **static** sections. 60 | 61 | #### Dynamic sections 62 | 63 | These are sections that can only exist on the homepage. The homepage is the only Shopify page that allows for an unlimited amount of sections that can be in whatever order the user wants. This allows the homepage to basically be completely editable in the CMS. 64 | 65 | #### Static sections 66 | 67 | Static sections are very similar to dynamic sections except they cannot be included on the homepage. There can only ever be on instance of these sections. A good example of a static section might be a hero that goes on the products listing page. This would allow a hero to be editable on the product listing pages but it cannot be used on any other pages. Adding to another page is technically possible but note that editing it's content in the CMS will result in updating the content for all instances of that section. 68 | 69 | ### Snippets 70 | 71 | Little re-usable bits of liquid. 72 | 73 | ## Working with JavaScript 74 | 75 | This base template utilizes a small library called [PicoApp](https://github.com/estrattonbailey/picoapp) to help organize your JS into components and easily maintain a global state. Please read through PicoApp's documentation before starting to work with the JavaScript. 76 | 77 | ## Working with SCSS 78 | 79 | This base template uses SCSS rather than plain CSS. The main scss file is appropriately called main.scss. It is imported into the index.js so that livereload can function properly. Feel free to add as many SCSS files as you like. Just import them into main.scss so that they get compiled correctly. 80 | 81 | ## Feedback 82 | 83 | We appreciate all feedback. Please contribute by creating pull requets or submitting issues. 84 | 85 | --- 86 | 87 | By [Use All Five](https://useallfive.com) 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@useallfive/slater-base", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "start": "slater watch", 6 | "build": "slater build", 7 | "deploy:development": "slater build && slater sync", 8 | "deploy:production": "slater build && slater sync --theme production", 9 | "test:start": "slater watch --config test.config.js", 10 | "test:build": "slater build --config test.config.js", 11 | "test:deploy": "slater build --config test.config.js && slater sync --config test.config.js", 12 | "test:deploy:production": "slater build --config test.config.js && slater sync --theme production --config test.config.js" 13 | }, 14 | "author": "https://thecouch.nyc", 15 | "license": "MIT", 16 | "homepage": "https://github.com/the-couch/slater#readme", 17 | "devDependencies": { 18 | "babel-eslint": "^10.1.0", 19 | "eslint": "^6.8.0", 20 | "eslint-config-prettier": "^6.10.1", 21 | "eslint-plugin-import": "^2.20.2", 22 | "eslint-plugin-prettier": "^3.1.3", 23 | "prettier": "^2.0.4", 24 | "slater": "^1.8.0" 25 | }, 26 | "dependencies": { 27 | "choozy": "0.0.6", 28 | "gridlex": "^2.7.1", 29 | "lazim": "^2.0.1", 30 | "mitt": "^1.2.0", 31 | "picoapp": "^3.6.1", 32 | "sliced": "^1.0.1", 33 | "unfetch": "^4.1.0" 34 | }, 35 | "gitHead": "59009c48bf724a0f868261cc0154c757f82aea14" 36 | } 37 | -------------------------------------------------------------------------------- /sample.slater.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | assets: { 5 | presets: ['sass'], 6 | }, 7 | themes: { 8 | development: { 9 | id: 'ID', 10 | password: 'PASSWORD', 11 | store: 'NAME.myshopify.com', 12 | ignore: [ 13 | 'settings_data.json', // leave this here to avoid overriding theme settings on sync 14 | ], 15 | }, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/assets/fonts.css.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | Use this file for handling Custom Fonts, don't add fonts. 3 | We don't support importing fonts from the Shopify pipeline 4 | so you have to do it here. 5 | {% endcomment %} -------------------------------------------------------------------------------- /src/assets/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | } 5 | * { 6 | box-sizing: border-box; 7 | } 8 | -------------------------------------------------------------------------------- /src/config/settings_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "current": "Default", 3 | "presets": { 4 | "Default": { 5 | "sections": { 6 | "collection-list": { 7 | "type": "collection-list", 8 | "blocks": { 9 | "collection-list-0": { 10 | "type": "collection" 11 | }, 12 | "collection-list-1": { 13 | "type": "collection" 14 | }, 15 | "collection-list-2": { 16 | "type": "collection" 17 | } 18 | }, 19 | "block_order": [ 20 | "collection-list-0", 21 | "collection-list-1", 22 | "collection-list-2" 23 | ] 24 | }, 25 | "featured-collection": { 26 | "type": "featured-collection", 27 | "settings": { 28 | "collection": "frontpage" 29 | } 30 | }, 31 | "featured-product": { 32 | "type": "featured-product" 33 | } 34 | }, 35 | "content_for_index": [ 36 | "collection-list", 37 | "featured-collection", 38 | "featured-product" 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/config/settings_schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "theme_info", 4 | "theme_name": "Slater", 5 | "theme_version": "1.0.0", 6 | "theme_author": "The Couch", 7 | "theme_documentation_url": "https://github.com/the-couch/slater-theme", 8 | "theme_support_url": "https://github.com/the-couch/slater-theme" 9 | }, 10 | { 11 | "name": "Typography", 12 | "settings": [ 13 | { 14 | "type": "select", 15 | "id": "type_base_size", 16 | "label": "Base size", 17 | "default": "16px", 18 | "options": [ 19 | { 20 | "value": "14px", 21 | "label": "14px" 22 | }, 23 | { 24 | "value": "15px", 25 | "label": "15px" 26 | }, 27 | { 28 | "value": "16px", 29 | "label": "16px" 30 | }, 31 | { 32 | "value": "17px", 33 | "label": "17px" 34 | } 35 | ] 36 | } 37 | ] 38 | }, 39 | { 40 | "name": "Colors", 41 | "settings": [ 42 | { 43 | "type": "header", 44 | "content": "General colors" 45 | }, 46 | { 47 | "type": "color", 48 | "id": "color_primary", 49 | "label": "Primary color", 50 | "default": "#000", 51 | "info": "Used for text links, and primary buttons" 52 | }, 53 | { 54 | "type": "color", 55 | "id": "color_body_text", 56 | "label": "Text color", 57 | "default": "#000", 58 | "info": "Color used for content and paragraph text" 59 | } 60 | ] 61 | }, 62 | { 63 | "name": "Social media", 64 | "settings": [ 65 | { 66 | "type": "header", 67 | "content": "Social sharing options" 68 | }, 69 | { 70 | "type": "checkbox", 71 | "id": "social_sharing_blog", 72 | "label": "Enable sharing for blog articles", 73 | "default": true 74 | }, 75 | { 76 | "type": "checkbox", 77 | "id": "share_facebook", 78 | "label": "Share on Facebook", 79 | "default": true 80 | }, 81 | { 82 | "type": "checkbox", 83 | "id": "share_twitter", 84 | "label": "Tweet on Twitter", 85 | "default": true 86 | }, 87 | { 88 | "type": "checkbox", 89 | "id": "share_pinterest", 90 | "label": "Pin on Pinterest", 91 | "default": true 92 | }, 93 | { 94 | "type": "header", 95 | "content": "Sharing links" 96 | }, 97 | { 98 | "type": "text", 99 | "id": "social_twitter_link", 100 | "label": "Twitter link" 101 | }, 102 | { 103 | "type": "text", 104 | "id": "social_facebook_link", 105 | "label": "Facebook link" 106 | }, 107 | { 108 | "type": "text", 109 | "id": "social_pinterest_link", 110 | "label": "Pinterest link" 111 | }, 112 | { 113 | "type": "text", 114 | "id": "social_instagram_link", 115 | "label": "Instagram link" 116 | }, 117 | { 118 | "type": "text", 119 | "id": "social_snapchat_link", 120 | "label": "Snapchat link" 121 | }, 122 | { 123 | "type": "text", 124 | "id": "social_tumblr_link", 125 | "label": "Tumblr link" 126 | }, 127 | { 128 | "type": "text", 129 | "id": "social_youtube_link", 130 | "label": "Youtube link" 131 | }, 132 | { 133 | "type": "text", 134 | "id": "social_vimeo_link", 135 | "label": "Vimeo link" 136 | } 137 | ] 138 | }, 139 | { 140 | "name": "Cart", 141 | "settings": [ 142 | { 143 | "type": "checkbox", 144 | "id": "cart_notes_enable", 145 | "label": "Enable cart notes", 146 | "default": true 147 | } 148 | ] 149 | }, 150 | { 151 | "name": "Favicon", 152 | "settings": [ 153 | { 154 | "type": "image_picker", 155 | "id": "favicon", 156 | "label": "Favicon image", 157 | "info": "Will be scaled down to 32 x 32px" 158 | } 159 | ] 160 | } 161 | ] 162 | -------------------------------------------------------------------------------- /src/layout/gift_card.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% if settings.favicon != blank %} 14 | 15 | {% endif %} 16 | 17 | {%- assign formatted_initial_value = gift_card.initial_value | money_without_trailing_zeros: gift_card.currency -%} 18 | {%- assign formatted_initial_value_stripped = formatted_initial_value | strip_html -%} 19 | {{ 'gift_cards.issued.title' | t: value: formatted_initial_value_stripped, shop: shop.name }} 20 | 21 | 22 | 23 | {{ 'main.css' | asset_url | stylesheet_tag }} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 69 | 70 | {{ content_for_header }} 71 | 72 | 73 |
74 | {{ content_for_layout }} 75 |
76 | 77 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/layout/password.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% if settings.favicon != blank %} 14 | 15 | {% endif %} 16 | 17 | {{ shop.name }} 18 | 19 | {% if page_description %} 20 | 21 | {% endif %} 22 | 23 | {% include 'social-meta-tags' %} 24 | 25 | {{ 'theme.scss.css' | asset_url | stylesheet_tag }} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{ content_for_header }} 38 | 39 | 40 |
41 |

42 | 45 |

46 |
47 | 48 |
49 | {{ content_for_layout }} 50 |
51 | 52 | 61 | 62 |
63 |

{{ 'general.password_page.login_form_heading' | t }}

64 | {% form 'storefront_password' %} 65 | {{ form.errors | default_errors }} 66 | 67 | 68 | 71 | {% endform %} 72 |

{{ 'general.password_page.admin_link_html' | t }}

73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /src/layout/theme.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% capture seo_title %} 10 | {{ page_title }} 11 | {% if current_tags %} 12 | {%- assign meta_tags = current_tags | join: ', ' %} 13 | – 14 | {{ 'general.meta.tags' | t: tags: meta_tags -}} 15 | {% endif %} 16 | {% if current_page != 1 %} 17 | – 18 | {{ 'general.meta.page' | t: page: current_page }} 19 | {% endif %} 20 | {% unless page_title contains shop.name %} 21 | – 22 | {{ shop.name }} 23 | {% endunless %} 24 | {% endcapture %} 25 | {{ seo_title }} 26 | 27 | {% include 'head-meta' %} 28 | 29 | {% if template.directory == 'customers' %} 30 | 31 | {% endif %} 32 | 33 | {% include 'css-variables' %} 34 | 35 | 45 | 46 | {{ 'index.css' | asset_url | stylesheet_tag }} 47 | 48 | {{ content_for_header }} 49 | 50 | 51 | 52 | {% section 'header' %} 53 | 54 |
55 | {{ content_for_layout }} 56 | {% section 'footer' %} 57 |
58 | 59 |
60 | 61 | {% include 'cart-drawer' %} 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/locales/en.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "accessibility": { 4 | "skip_to_content": "Skip to content" 5 | }, 6 | "meta": { 7 | "tags": "Tagged \"{{ tags }}\"", 8 | "page": "Page {{ page }}" 9 | }, 10 | "404": { 11 | "title": "404 Page Not Found", 12 | "subtext_html": "The page you requested does not exist. Click here to continue shopping." 13 | }, 14 | "password_page": { 15 | "opening_soon": "Opening Soon", 16 | "spread_the_word": "Spread the word", 17 | "login_form_heading": "Enter store using password", 18 | "login_form_password_label": "Password", 19 | "login_form_password_placeholder": "Your password", 20 | "login_form_submit": "Enter", 21 | "signup_form_heading": "Find out when we open", 22 | "signup_form_email_label": "Email", 23 | "signup_form_email_placeholder": "Your email", 24 | "signup_form_submit": "Submit", 25 | "signup_form_success": "We will send you an email right before we open!", 26 | "admin_link_html": "Are you the store owner? Log in here", 27 | "password_link": "Enter using password", 28 | "powered_by_shopify_html": "This shop will be powered by {{ shopify }}" 29 | }, 30 | "social": { 31 | "share_on_facebook": "Share", 32 | "share_on_twitter": "Tweet", 33 | "share_on_pinterest": "Pin it", 34 | "alt_text": { 35 | "share_on_facebook": "Share on Facebook", 36 | "share_on_twitter": "Tweet on Twitter", 37 | "share_on_pinterest": "Pin on Pinterest" 38 | } 39 | }, 40 | "search": { 41 | "no_results_html": "Your search for \"{{ terms }}\" did not yield any results.", 42 | "results_for_html": "Your search for \"{{ terms }}\" revealed the following:", 43 | "title": "Search for products on our site", 44 | "placeholder": "Search our store", 45 | "submit": "Search" 46 | } 47 | }, 48 | "blogs": { 49 | "article": { 50 | "author_on_date_html": "Posted by {{ author }} on {{ date }}", 51 | "comment_meta_html": "{{ author }} on {{ date }}", 52 | "read_more": "Read more" 53 | }, 54 | "comments": { 55 | "title": "Leave a comment", 56 | "name": "Name", 57 | "email": "Email", 58 | "message": "Message", 59 | "post": "Post comment", 60 | "moderated": "Please note, comments must be approved before they are published", 61 | "success_moderated": "Your comment was posted successfully. We will publish it in a little while, as our blog is moderated.", 62 | "success": "Your comment was posted successfully! Thank you!", 63 | "with_count": { 64 | "one": "{{ count }} comment", 65 | "other": "{{ count }} comments" 66 | } 67 | }, 68 | "general": { 69 | "categories": "Categories" 70 | } 71 | }, 72 | "cart": { 73 | "general": { 74 | "title": "Shopping Cart", 75 | "remove": "Remove", 76 | "note": "Special instructions for seller", 77 | "subtotal": "Subtotal", 78 | "shipping_at_checkout": "Shipping & taxes calculated at checkout", 79 | "update": "Update Cart", 80 | "checkout": "Check Out", 81 | "empty": "Your cart is currently empty.", 82 | "cookies_required": "Enable cookies to use the shopping cart", 83 | "continue_browsing_html": "Continue browsing here.", 84 | "item_quantity": "Item quantity", 85 | "savings": "You're saving" 86 | }, 87 | "label": { 88 | "product": "Product", 89 | "price": "Price", 90 | "quantity": "Quantity", 91 | "total": "Total", 92 | "discounted_price": "Discounted price", 93 | "original_price": "Original price" 94 | } 95 | }, 96 | "collections": { 97 | "general": { 98 | "no_matches": "Sorry, there are no products in this collection", 99 | "link_title": "Browse our {{ title }} collection" 100 | } 101 | }, 102 | "contact": { 103 | "form": { 104 | "name": "Name", 105 | "email": "Email", 106 | "phone": "Phone Number", 107 | "message": "Message", 108 | "send": "Send", 109 | "post_success": "Thanks for contacting us. We'll get back to you as soon as possible." 110 | } 111 | }, 112 | "customer": { 113 | "account": { 114 | "title": "My Account", 115 | "details": "Account Details", 116 | "view_addresses": "View Addresses", 117 | "return": "Return to Account Details" 118 | }, 119 | "activate_account": { 120 | "title": "Activate Account", 121 | "subtext": "Create your password to activate your account.", 122 | "password": "Password", 123 | "password_confirm": "Confirm Password", 124 | "submit": "Activate Account", 125 | "cancel": "Decline Invitation" 126 | }, 127 | "addresses": { 128 | "title": "Your Addresses", 129 | "default": "Default", 130 | "add_new": "Add a New Address", 131 | "edit_address": "Edit address", 132 | "first_name": "First Name", 133 | "last_name": "Last Name", 134 | "company": "Company", 135 | "address1": "Address1", 136 | "address2": "Address2", 137 | "city": "City", 138 | "country": "Country", 139 | "province": "Province", 140 | "zip": "Postal/Zip Code", 141 | "phone": "Phone", 142 | "set_default": "Set as default address", 143 | "add": "Add Address", 144 | "update": "Update Address", 145 | "cancel": "Cancel", 146 | "edit": "Edit", 147 | "delete": "Delete", 148 | "delete_confirm": "Are you sure you wish to delete this address?" 149 | }, 150 | "login": { 151 | "title": "Login", 152 | "email": "Email", 153 | "password": "Password", 154 | "forgot_password": "Forgot your password?", 155 | "sign_in": "Sign In", 156 | "cancel": "Return to Store", 157 | "guest_title": "Continue as a guest", 158 | "guest_continue": "Continue" 159 | }, 160 | "orders": { 161 | "title": "Order History", 162 | "order_number": "Order", 163 | "date": "Date", 164 | "payment_status": "Payment Status", 165 | "fulfillment_status": "Fulfillment Status", 166 | "total": "Total", 167 | "none": "You haven't placed any orders yet." 168 | }, 169 | "order": { 170 | "title": "Order {{ name }}", 171 | "date": "Placed on {{ date }}", 172 | "cancelled": "Order Cancelled on {{ date }}", 173 | "cancelled_reason": "Reason: {{ reason }}", 174 | "billing_address": "Billing Address", 175 | "payment_status": "Payment Status", 176 | "shipping_address": "Shipping Address", 177 | "fulfillment_status": "Fulfillment Status", 178 | "discount": "Discount", 179 | "shipping": "Shipping", 180 | "tax": "Tax", 181 | "product": "Product", 182 | "sku": "SKU", 183 | "price": "Price", 184 | "quantity": "Quantity", 185 | "total": "Total", 186 | "fulfilled_at": "Fulfilled {{ date }}", 187 | "subtotal": "Subtotal" 188 | }, 189 | "recover_password": { 190 | "title": "Reset your password", 191 | "email": "Email", 192 | "submit": "Submit", 193 | "cancel": "Cancel", 194 | "subtext": "We will send you an email to reset your password.", 195 | "success": "We've sent you an email with a link to update your password." 196 | }, 197 | "reset_password": { 198 | "title": "Reset account password", 199 | "subtext": "Enter a new password for {{ email }}", 200 | "password": "Password", 201 | "password_confirm": "Confirm Password", 202 | "submit": "Reset Password" 203 | }, 204 | "register": { 205 | "title": "Create Account", 206 | "first_name": "First Name", 207 | "last_name": "Last Name", 208 | "email": "Email", 209 | "password": "Password", 210 | "submit": "Create", 211 | "cancel": "Return to Store" 212 | } 213 | }, 214 | "homepage": { 215 | "onboarding": { 216 | "product_title": "Example Product Title", 217 | "collection_title": "Example Collection Title", 218 | "no_content": "This section doesn’t currently include any content. Add content to this section using the sidebar." 219 | } 220 | }, 221 | "layout": { 222 | "cart": { 223 | "title": "Cart", 224 | "items_count": { 225 | "one": "item", 226 | "other": "items" 227 | } 228 | }, 229 | "customer": { 230 | "account": "Account", 231 | "logged_in_as_html": "Logged in as {{ first_name }}", 232 | "log_out": "Log out", 233 | "log_in": "Log in", 234 | "create_account": "Create account" 235 | }, 236 | "footer": { 237 | "social_platform": "{{ name }} on {{ platform }}", 238 | "copyright": "Copyright", 239 | "payment_methods": "Payment methods accepted" 240 | } 241 | }, 242 | "products": { 243 | "product": { 244 | "regular_price": "Regular price", 245 | "sold_out": "Sold Out", 246 | "unavailable": "Unavailable", 247 | "on_sale": "On Sale", 248 | "on_sale_from_html": "On Sale from {{ price }}", 249 | "from_text_html": "From {{ price }}", 250 | "quantity": "Quantity", 251 | "add_to_cart": "Add to Cart" 252 | } 253 | }, 254 | "gift_cards": { 255 | "issued": { 256 | "title": "Here's your {{ value }} gift card for {{ shop }}!", 257 | "subtext": "Here's your gift card!", 258 | "disabled": "Disabled", 259 | "expired": "Expired on {{ expiry }}", 260 | "active": "Expires on {{ expiry }}", 261 | "redeem": "Use this code at checkout to redeem your gift card", 262 | "shop_link": "Start shopping", 263 | "print": "Print", 264 | "remaining_html": "{{ balance }} left", 265 | "add_to_apple_wallet": "Add to Apple Wallet" 266 | } 267 | }, 268 | "date_formats":{ 269 | "month_day_year": "%B %d, %Y" 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/scripts/app.js: -------------------------------------------------------------------------------- 1 | import { picoapp } from 'picoapp' 2 | 3 | import slaterWelcome from '@/components/slater-welcome.js' 4 | import hero from '@/components/hero.js' 5 | 6 | import header from '@/components/header.js' 7 | import productSelection from '@/components/product-selection.js' 8 | import cartDrawer from '@/components/cartDrawer.js' 9 | import cartDrawerItem from '@/components/cartDrawerItem.js' 10 | import accountLogin from '@/components/accountLogin.js' 11 | import product from '@/components/product.js' 12 | import productCounter from '@/components/product-counter.js' 13 | 14 | const state = { 15 | cartOpen: false, 16 | } 17 | 18 | const components = { 19 | slaterWelcome, 20 | hero, 21 | accountLogin, 22 | header, 23 | productSelection, 24 | cartDrawer, 25 | cartDrawerItem, 26 | product, 27 | productCounter, 28 | } 29 | 30 | export default picoapp(components, state) 31 | -------------------------------------------------------------------------------- /src/scripts/components/accountLogin.js: -------------------------------------------------------------------------------- 1 | import { component } from 'picoapp' 2 | 3 | export default component((node) => { 4 | const login = node.querySelector('.js-login-dialog') 5 | const recover = node.querySelector('.js-recover-dialog') 6 | const recoverLink = node.querySelector('.js-recover-trigger') 7 | const cancelRecoverLink = node.querySelector('.js-recover-cancel') 8 | 9 | /* eslint-disable */ 10 | const recoverIsTarget = window.location.hash.match(/\#recover/) ? true : false 11 | /* eslint-enable */ 12 | const successMessage = node.querySelector('.js-recover-success') !== null 13 | 14 | if (recoverIsTarget || successMessage) { 15 | login.style.display = 'none' 16 | recover.style.display = 'block' 17 | } else { 18 | login.style.display = 'block' 19 | } 20 | 21 | recoverLink.addEventListener('click', (e) => { 22 | e.preventDefault() 23 | login.style.display = 'none' 24 | recover.style.display = 'block' 25 | }) 26 | 27 | cancelRecoverLink.addEventListener('click', (e) => { 28 | e.preventDefault() 29 | recover.style.display = 'none' 30 | login.style.display = 'block' 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/scripts/components/cartDrawer.js: -------------------------------------------------------------------------------- 1 | import { component } from 'picoapp' 2 | import { getSizedImageUrl, imageSize } from '@/lib/images.js' 3 | import { formatMoney } from '@/lib/currency.js' 4 | import app from '@/app.js' 5 | 6 | /* eslint-disable-next-line max-len */ 7 | const X = `` 8 | 9 | function createItem({ 10 | variant_id: id, 11 | product_title: title, 12 | line_price: price, 13 | variant_title: color, 14 | image, 15 | url, 16 | quantity, 17 | ...item // eslint-disable-line 18 | }) { 19 | const img = image 20 | ? getSizedImageUrl( 21 | image.replace('.' + imageSize(image), ''), 22 | '200x' // TODO hacky af 23 | ) 24 | : 'https://source.unsplash.com/R9OS29xJb-8/2000x1333' 25 | 26 | return ` 27 |
28 |
29 | 30 | 31 | 32 |
33 |
34 | ${title} 35 |
${formatMoney(price)}
36 |
37 |
-
38 |
${quantity}
39 |
+
40 |
41 | ${color ? `
${color.split(':')[0]}
` : ``} 42 |
43 | 44 | 45 |
46 |
47 |
48 | ` 49 | } 50 | 51 | function renderItems(items) { 52 | return items.length > 0 53 | ? items.reduce((markup, item) => { 54 | markup += createItem(item) 55 | return markup 56 | }, '') 57 | : `

Your cart is empty

` 58 | } 59 | 60 | export default component((node, ctx) => { 61 | const overlay = node.querySelector('.js-overlay') 62 | const closeButton = node.querySelector('.js-close') 63 | const subtotal = node.querySelector('.js-subtotal') 64 | const itemsRoot = node.querySelector('.js-items') 65 | const loading = itemsRoot.innerHTML 66 | 67 | const render = (cart) => { 68 | itemsRoot.innerHTML = renderItems(cart.items) 69 | subtotal.innerHTML = formatMoney(cart.total_price) 70 | } 71 | 72 | const open = (cart) => { 73 | node.classList.add('is-active') 74 | itemsRoot.innerHTML = loading 75 | setTimeout(() => { 76 | node.classList.add('is-visible') 77 | setTimeout(render(cart), 10) 78 | app.mount() 79 | }, 50) 80 | } 81 | 82 | const close = () => { 83 | node.classList.remove('is-visible') 84 | setTimeout(() => { 85 | node.classList.remove('is-active') 86 | app.hydrate({ cartOpen: false }) 87 | }, 400) 88 | } 89 | 90 | render(ctx.getState().cart) 91 | 92 | overlay.addEventListener('click', close) 93 | closeButton.addEventListener('click', close) 94 | 95 | ctx.on('cart:toggle', ({ cart, cartOpen }) => { 96 | if (cartOpen) open(cart) 97 | }) 98 | ctx.on('cart:updated', () => { 99 | render(ctx.getState().cart) 100 | app.mount() 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /src/scripts/components/cartDrawerItem.js: -------------------------------------------------------------------------------- 1 | import { component } from 'picoapp' 2 | import { removeAddon, updateAddon } from '@/lib/cart.js' 3 | 4 | export default component((node) => { 5 | const button = node.querySelector('.js-remove-item') 6 | const decrease = node.querySelector('.js-remove-single') 7 | const increase = node.querySelector('.js-add-single') 8 | const currentQty = node.querySelector('.js-single-quantity').innerHTML 9 | const id = node.getAttribute('data-id') 10 | 11 | button.addEventListener('click', () => { 12 | removeAddon(id) 13 | }) 14 | 15 | decrease.addEventListener('click', (e) => { 16 | e.preventDefault() 17 | updateAddon(id, parseInt(currentQty) - 1) 18 | }) 19 | 20 | increase.addEventListener('click', (e) => { 21 | e.preventDefault() 22 | updateAddon(id, parseInt(currentQty) + 1) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/scripts/components/header.js: -------------------------------------------------------------------------------- 1 | import { component } from 'picoapp' 2 | 3 | export default component((node, ctx) => { 4 | const cartCount = node.querySelector('.js-cart-count') 5 | const cartToggles = node.querySelectorAll('.js-cart-drawer-toggle') 6 | 7 | for (let i = 0; i < cartToggles.length; i++) { 8 | cartToggles[i].addEventListener('click', (e) => { 9 | e.preventDefault() 10 | 11 | ctx.emit('cart:toggle', (state) => { 12 | return { 13 | cartOpen: !state.cartOpen, 14 | } 15 | }) 16 | }) 17 | } 18 | 19 | ctx.on('cart:updated', (state) => { 20 | cartCount.innerHTML = state.cart.item_count 21 | }) 22 | 23 | cartCount.innerHTML = ctx.getState().cart.item_count 24 | }) 25 | -------------------------------------------------------------------------------- /src/scripts/components/hero.js: -------------------------------------------------------------------------------- 1 | import { component } from 'picoapp' 2 | 3 | export default component(() => { 4 | console.log('hero mounted') 5 | 6 | return () => { 7 | console.log('hero unmounted') 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /src/scripts/components/product-counter.js: -------------------------------------------------------------------------------- 1 | import { component } from 'picoapp' 2 | 3 | export default component((node) => { 4 | const decrease = node.querySelector('.js-counter-remove') 5 | const increase = node.querySelector('.js-counter-add') 6 | const quantity = node.querySelector('.js-counter-quantity') 7 | 8 | const min = parseInt(quantity.attributes.min.value) 9 | const max = parseInt(quantity.attributes.max.value) 10 | 11 | let count = parseInt(quantity.value) 12 | 13 | const set = (i) => { 14 | count = Math.max(min, Math.min(i, max || 10000)) 15 | quantity.value = count 16 | } 17 | 18 | decrease.addEventListener('click', (e) => { 19 | e.preventDefault() 20 | set(--count) 21 | }) 22 | 23 | increase.addEventListener('click', (e) => { 24 | e.preventDefault() 25 | set(++count) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/scripts/components/product-selection.js: -------------------------------------------------------------------------------- 1 | import { component } from 'picoapp' 2 | import options from '@/lib/options.js' 3 | import getProductJson from '@/lib/getProductJson.js' 4 | 5 | export default component(({ node }) => { 6 | const opts = options(node) 7 | 8 | // cache 9 | getProductJson() 10 | 11 | opts.onUpdate((state) => { 12 | getProductJson().then((json) => { 13 | const variant = json.variants.filter((v) => v.id == state.id)[0] 14 | console.log(variant) 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/scripts/components/product.js: -------------------------------------------------------------------------------- 1 | import { component } from 'picoapp' 2 | import { addVariant } from '@/lib/cart.js' 3 | 4 | export default component((node) => { 5 | const json = JSON.parse(node.querySelector('.js-product-json').innerHTML) 6 | const form = node.querySelector('form') 7 | 8 | const { selectedOrFirstAvailableVariant, product } = JSON.parse( 9 | document.querySelector('.js-product-json').innerHTML 10 | ) 11 | let currentVariant = product.variants.filter((v) => v.id === selectedOrFirstAvailableVariant)[0] 12 | 13 | form.addEventListener('submit', (e) => { 14 | e.preventDefault() 15 | currentVariant = product.variants.filter((v) => v.id === parseInt(form.elements.id.value))[0] 16 | 17 | addVariant(currentVariant, form.elements.quantity.value) 18 | console.log(json) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/scripts/components/slater-welcome.js: -------------------------------------------------------------------------------- 1 | import { component } from 'picoapp' 2 | 3 | export default component(() => { 4 | console.log('slater-welcome mounted') 5 | 6 | return () => { 7 | console.log('slater-welcome unmounted') 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /src/scripts/index.js: -------------------------------------------------------------------------------- 1 | import '../styles/main.scss' 2 | import '@/lib/select.js' 3 | import app from '@/app.js' 4 | import { fetchCart } from '@/lib/cart.js' 5 | 6 | /** 7 | * load any data that your site needs on fist load 8 | */ 9 | Promise.all([fetchCart()]).then(([cart]) => { 10 | /** 11 | * add the cart data to the picoapp instance 12 | */ 13 | app.hydrate({ cart }) 14 | 15 | /** 16 | * mount any components defined on the page 17 | */ 18 | app.mount() 19 | }) 20 | -------------------------------------------------------------------------------- /src/scripts/lib/cart.js: -------------------------------------------------------------------------------- 1 | import fetch from 'unfetch' 2 | import app from '@/app.js' 3 | 4 | export function addVariant(variant, quantity) { 5 | const numAvailable = 6 | variant.inventory_policy === 'deny' && variant.inventory_management === 'shopify' 7 | ? variant.inventory_quantity 8 | : null // null means they can add as many as they want 9 | 10 | return fetchCart().then(({ items }) => { 11 | const existing = items.filter((item) => item.id === variant.id)[0] || {} 12 | const numRequested = (existing.quantity || 0) + quantity 13 | 14 | if (numAvailable !== null && numRequested > numAvailable) { 15 | const err = `There are only ${numAvailable} of that product available, requested ${numRequested}.` 16 | app.emit('error', err) 17 | throw new Error(err) 18 | } else { 19 | return addItemById(variant.id, quantity) 20 | } 21 | }) 22 | } 23 | 24 | export async function updateAddon(id, quantity) { 25 | const { items } = await fetchCart() 26 | for (let i = 0; i < items.length; i++) { 27 | if (items[i].variant_id === parseInt(id)) { 28 | return changeAddon(i + 1, quantity) // shopify cart is a 1-based index 29 | } 30 | } 31 | } 32 | 33 | export function removeAddon(id) { 34 | return updateAddon(id, 0) 35 | } 36 | 37 | function changeAddon(line, quantity) { 38 | app.emit('cart:updating') 39 | 40 | return fetch('/cart/change.js', { 41 | method: 'POST', 42 | credentials: 'include', 43 | headers: { 44 | 'Content-Type': 'application/json', 45 | }, 46 | body: JSON.stringify({ line, quantity }), 47 | }) 48 | .then((res) => res.json()) 49 | .then((cart) => { 50 | app.hydrate({ cart: cart }) 51 | app.emit('cart:updated', { cart: cart }) 52 | return cart 53 | }) 54 | } 55 | 56 | /** 57 | * Warning: this does not check available products first 58 | */ 59 | export function addItemById(id, quantity) { 60 | app.emit('cart:updating') 61 | 62 | return fetch('/cart/add.js', { 63 | method: 'POST', 64 | credentials: 'include', 65 | headers: { 66 | 'Content-Type': 'application/json', 67 | }, 68 | body: JSON.stringify({ id, quantity }), 69 | }) 70 | .then((r) => r.json()) 71 | .then((item) => { 72 | return fetchCart().then((cart) => { 73 | app.hydrate({ cart: cart }) 74 | app.emit('cart:updated') 75 | app.emit('cart:toggle', (state) => { 76 | return { 77 | cartOpen: !state.cartOpen, 78 | } 79 | }) 80 | // app.emit('updated', { item, cart }) 81 | return { item, cart } 82 | }) 83 | }) 84 | } 85 | 86 | export function fetchCart() { 87 | return fetch('/cart.js', { 88 | method: 'GET', 89 | credentials: 'include', 90 | }).then((res) => res.json()) 91 | } 92 | -------------------------------------------------------------------------------- /src/scripts/lib/choozy.js: -------------------------------------------------------------------------------- 1 | import choozy from 'choozy' 2 | 3 | export default (node) => choozy(node) 4 | -------------------------------------------------------------------------------- /src/scripts/lib/currency.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Currency Helpers 3 | * ----------------------------------------------------------------------------- 4 | * A collection of useful functions that help with currency formatting 5 | * 6 | * Current contents 7 | * - formatMoney - Takes an amount in cents and returns it as a formatted dollar value. 8 | * 9 | */ 10 | 11 | /** 12 | * Format money values based on your shop currency settings 13 | * @param {Number|string} cents - value in cents or dollar amount e.g. 300 cents 14 | * or 3.00 dollars 15 | * @param {String} format - shop money_format setting 16 | * @return {String} value - formatted value 17 | */ 18 | 19 | /* eslint-disable */ 20 | 21 | export function formatMoney(cents, format = '${{amount}}') { 22 | if (typeof cents === 'string') { 23 | cents = cents.replace('.', '') 24 | } 25 | 26 | let value = '' 27 | const placeholderRegex = /\{\{\s*(\w+)\s*\}\}/ 28 | 29 | function formatWithDelimiters(number, precision, thousands, decimal) { 30 | precision = precision || 2 31 | thousands = thousands || ',' 32 | decimal = decimal || '.' 33 | 34 | if (isNaN(number) || number == null) { 35 | return 0 36 | } 37 | 38 | number = (number / 100.0).toFixed(precision) 39 | 40 | const parts = number.split('.') 41 | const dollarsAmount = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + thousands) 42 | const centsAmount = parts[1] ? decimal + parts[1] : '' 43 | 44 | return dollarsAmount + centsAmount 45 | } 46 | 47 | switch (format.match(placeholderRegex)[1]) { 48 | case 'amount': 49 | value = formatWithDelimiters(cents, 2) 50 | break 51 | case 'amount_no_decimals': 52 | value = formatWithDelimiters(cents, 0) 53 | break 54 | case 'amount_with_space_separator': 55 | value = formatWithDelimiters(cents, 2, ' ', '.') 56 | break 57 | case 'amount_no_decimals_with_comma_separator': 58 | value = formatWithDelimiters(cents, 0, ',', '.') 59 | break 60 | case 'amount_no_decimals_with_space_separator': 61 | value = formatWithDelimiters(cents, 0, ' ') 62 | break 63 | } 64 | 65 | return format.replace(placeholderRegex, value) 66 | } 67 | -------------------------------------------------------------------------------- /src/scripts/lib/getProductJson.js: -------------------------------------------------------------------------------- 1 | const cache = {} 2 | 3 | export default function getProductJson(slug = window.location.pathname.split('/').reverse()[0], opts = {}) { 4 | if (cache[slug] && !opts.refetch) return Promise.resolve(cache[slug]) 5 | 6 | return fetch(window.location.origin + '/products/' + slug + '.json') 7 | .then((res) => res.json()) 8 | .then(({ product }) => { 9 | cache[slug] = product 10 | return product 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /src/scripts/lib/images.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Image Helper Functions 3 | * ----------------------------------------------------------------------------- 4 | * A collection of functions that help with basic image operations. 5 | * 6 | */ 7 | 8 | /** 9 | * Preloads an image in memory and uses the browsers cache to store it until needed. 10 | * 11 | * @param {Array} images - A list of image urls 12 | * @param {String} size - A shopify image size attribute 13 | */ 14 | export function preload(images, size) { 15 | if (typeof images === 'string') { 16 | images = [images] 17 | } 18 | 19 | for (var i = 0; i < images.length; i++) { 20 | var image = images[i] 21 | loadImage(getSizedImageUrl(image, size)) 22 | } 23 | } 24 | 25 | /** 26 | * Loads and caches an image in the browsers cache. 27 | * @param {string} path - An image url 28 | */ 29 | export function loadImage(path) { 30 | /* eslint-disable */ 31 | new Image().src = path 32 | /* eslint-enable */ 33 | } 34 | 35 | /** 36 | * Find the Shopify image attribute size 37 | * 38 | * @param {string} src 39 | * @returns {null} 40 | */ 41 | export function imageSize(src) { 42 | /* eslint-disable */ 43 | var match = src.match(/.+_((?:pico|icon|thumb|small|compact|medium|large|grande)|\d{1,4}x\d{0,4}|x\d{1,4})[_\.@]/) 44 | /* esling-enable */ 45 | 46 | if (match) { 47 | return match[1] 48 | } else { 49 | return null 50 | } 51 | } 52 | 53 | /** 54 | * Adds a Shopify size attribute to a URL 55 | * 56 | * @param src 57 | * @param size 58 | * @returns {*} 59 | */ 60 | export function getSizedImageUrl(src, size) { 61 | if (size === null) { 62 | return src 63 | } 64 | 65 | if (size === 'master') { 66 | return removeProtocol(src) 67 | } 68 | 69 | var match = src.match(/\.(jpg|jpeg|gif|png|bmp|bitmap|tiff|tif)(\?v=\d+)?$/i) 70 | 71 | if (match) { 72 | var prefix = src.split(match[0]) 73 | var suffix = match[0] 74 | 75 | return removeProtocol(prefix[0] + '_' + size + suffix) 76 | } else { 77 | return null 78 | } 79 | } 80 | 81 | export function removeProtocol(path) { 82 | return path.replace(/http(s)?:/, '') 83 | } 84 | -------------------------------------------------------------------------------- /src/scripts/lib/options.js: -------------------------------------------------------------------------------- 1 | import radio from '@/lib/radio.js' 2 | 3 | export default function productSelection(node, opts) { 4 | opts = Object.assign( 5 | { 6 | select: '[data-option-select]', 7 | radio: '[data-option-radio]', 8 | main: '[data-option-main]', 9 | }, 10 | opts 11 | ) 12 | 13 | const listeners = [] 14 | 15 | const state = { 16 | id: null, 17 | options: [], 18 | } 19 | 20 | const selects = slater.qsa(opts.select) 21 | const radios = slater.qsa(opts.radio) 22 | const main = slater.qs(opts.main) 23 | 24 | if (!main || !main.length) throw 'data-option-main is missing' 25 | if (radios.length > 3) throw 'you have more than three radio groups' 26 | if (selects.length > 3) throw 'you have more than three select inputs' 27 | 28 | const variants = [].slice.call(main.children).reduce((variants, child) => { 29 | variants[child.innerHTML] = child.value 30 | return variants 31 | }, {}) 32 | 33 | selects.forEach((select) => { 34 | if (select.nodeName !== 'SELECT') 35 | throw 'data-option-select should be defined on the individual option selectors' 36 | 37 | const index = parseInt(select.getAttribute('data-index')) 38 | 39 | // set initial value 40 | state.options[index] = select.value 41 | 42 | select.addEventListener('change', (e) => { 43 | state.options[index] = e.target.value 44 | updateSelection() 45 | }) 46 | }) 47 | 48 | radios.forEach((r) => { 49 | if (r.nodeName === 'INPUT') 50 | throw 'data-option-radio should be defined on a parent of the radio group, not the inputs themselves' 51 | 52 | const index = parseInt(r.getAttribute('data-index')) 53 | const inputs = [].slice.call(r.getElementsByTagName('input')) 54 | 55 | // set initial value 56 | inputs.forEach((r) => { 57 | if (r.checked) state.options[index] = r.value 58 | }) 59 | 60 | radio(inputs, (value) => { 61 | state.options[index] = value 62 | updateSelection() 63 | }) 64 | }) 65 | 66 | updateSelection() 67 | 68 | function updateSelection() { 69 | state.id = variants[state.options.join(' / ')] 70 | main.value = state.id 71 | for (let fn of listeners) fn(state) 72 | } 73 | 74 | return { 75 | get state() { 76 | return state 77 | }, 78 | onUpdate(fn) { 79 | if (listeners.indexOf(fn) < 0 && listeners.push(fn)) { 80 | return () => listeners.splice(listeners.indexOf(fn), 1) 81 | } 82 | }, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/scripts/lib/product-selector.js: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt' 2 | 3 | export default function ProductSelector( 4 | config = { 5 | main: '[data-product-select]', 6 | options: '[data-single-option-selector]', 7 | data: '[data-product-json]', 8 | } 9 | ) { 10 | const ev = mitt() 11 | 12 | const main = document.querySelector(config.main) 13 | const options = [].slice.call(document.querySelectorAll(config.options)) 14 | const data = JSON.parse(document.querySelector(config.data).innerHTML) 15 | 16 | options.forEach((opt) => 17 | opt.addEventListener('change', () => { 18 | const val = options.reduce((res, opt, i) => { 19 | res += i < options.length - 1 ? opt.value + ' / ' : opt.value 20 | return res 21 | }, '') 22 | 23 | for (let i = 0; i < main.options.length; i++) { 24 | if (main.options[i].text === val) { 25 | main.selectedIndex = i 26 | break 27 | } 28 | } 29 | 30 | ev.emit('update', data.variants.filter((v) => v.title === val)[0]) 31 | }) 32 | ) 33 | 34 | return { 35 | on: ev.on, 36 | destroy() { 37 | options.forEach(() => { 38 | // opt.removeEventListener('change', updateSelect) 39 | }) 40 | }, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/scripts/lib/radio.js: -------------------------------------------------------------------------------- 1 | export default function radio(radios, cb) { 2 | radios.map((r) => (r.onclick = (e) => cb(e.target.value))) 3 | } 4 | -------------------------------------------------------------------------------- /src/scripts/lib/select.js: -------------------------------------------------------------------------------- 1 | import sliced from 'sliced' 2 | 3 | window.slater = Object.assign(window.slater || {}, { 4 | qs(q, ctx) { 5 | return (ctx || document).querySelector(q) 6 | }, 7 | qsa(q, ctx) { 8 | return sliced((ctx || document).querySelectorAll(q)) 9 | }, 10 | gebtn(q, ctx) { 11 | return sliced((ctx || document).getElementsByTagName(q)) 12 | }, 13 | gebi(q) { 14 | return document.getElementById(q) 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/sections/footer.liquid: -------------------------------------------------------------------------------- 1 | 26 | 27 | {% schema %} 28 | { 29 | "name": "Footer", 30 | "settings": [ 31 | { 32 | "type": "link_list", 33 | "id": "footer_menu", 34 | "label": "Menu", 35 | "default": "footer" 36 | } 37 | ] 38 | } 39 | {% endschema %} 40 | -------------------------------------------------------------------------------- /src/sections/header.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} Considerations for the header: 1. Schema.org tags - 2 |

3 | - itemprop [url, logo] etc {% endcomment %} 4 | 5 | 22 | 23 |
24 |

25 | -------------------------------------------------------------------------------- /src/sections/hero-collection.liquid: -------------------------------------------------------------------------------- 1 | {% include 'hero' %} 2 | 3 | {% schema %} 4 | { 5 | "name": "Hero", 6 | "settings": [ 7 | { 8 | "id": "title", 9 | "type": "text", 10 | "label": "Title", 11 | "default": "Find which products are right for you" 12 | } 13 | ] 14 | } 15 | {% endschema %} 16 | -------------------------------------------------------------------------------- /src/sections/hero.liquid: -------------------------------------------------------------------------------- 1 | {% include 'hero' %} 2 | 3 | {% schema %} 4 | { 5 | "name": "Hero", 6 | "settings": [ 7 | { 8 | "id": "title", 9 | "type": "text", 10 | "label": "Title" 11 | } 12 | ], 13 | "presets": [ 14 | { 15 | "name": "Hero", 16 | "category": "hero" 17 | } 18 | ] 19 | } 20 | {% endschema %} 21 | -------------------------------------------------------------------------------- /src/sections/slater-welcome.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

A Shopify theme development toolkit

5 |

A barebones theme and handy CLI for easy build and deployment.

6 |
npm i slater -g
7 | 8 |
9 | github 10 | npm 11 |
12 |
13 |
14 |
15 | 16 | {% schema %} 17 | { 18 | "name": "Welcome", 19 | "settings": [], 20 | "presets": [ 21 | { 22 | "name": "Welcome", 23 | "category": "slater" 24 | } 25 | ] 26 | } 27 | {% endschema %} 28 | -------------------------------------------------------------------------------- /src/snippets/account-address-form.liquid: -------------------------------------------------------------------------------- 1 | 153 | 154 | 246 | -------------------------------------------------------------------------------- /src/snippets/account-address.liquid: -------------------------------------------------------------------------------- 1 |
2 | {% if default %} 3 |
Default
4 | {% endif %} 5 | 6 |
7 |
{{ address.name }}
8 | {% if address.company %} 9 | {{ address.company }}
10 | {% endif %} 11 | {{ address.street }}
12 | {{ address.city | capitalize }} 13 | {% if address.province %}, 14 | {{ address.province | upcase }} 15 | {% endif %} 16 | {{ address.zip | upcase }}
17 | {{ address.country }}
18 | {% if address.phone != '' %} 19 | {{ address.phone }} 20 | {% endif %} 21 |
22 | 23 | {% if edit %} 24 | 28 | {% endif %} 29 |
30 | -------------------------------------------------------------------------------- /src/snippets/account-titles.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |

{{ title }}

7 | 8 | 31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /src/snippets/cart-drawer.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 9 | 10 |

Your Cart

11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 |
23 | 24 | 37 |
38 |
39 | -------------------------------------------------------------------------------- /src/snippets/component-button.liquid: -------------------------------------------------------------------------------- 1 | {%- capture cx -%} 2 | button rel 3 | {{ className }} 4 | {% if outline == true %}button--outline 5 | {% endif %} 6 | {% if small == true %}button--small 7 | {% endif %} 8 | {%- endcapture -%} 9 | 10 | <{{tag|default:'a'}} {% if href %} href='{{ href }}' {% endif %} {% if name %} name='{{ name }}' {% endif %} {% if disabled %} disabled {% endif %} type='{{ type | default: 'button' }}' class='{{ cx | strip }}' role='button'> 11 | {{ cta | strip }} 12 | 13 | 14 | {% assign cta = blank %} 15 | {% assign className = blank %} 16 | {% assign href = blank %} 17 | {% assign name = blank %} 18 | {% assign disabled = false %} 19 | {% assign type = 'button' %} 20 | {% assign cx = '' %} 21 | -------------------------------------------------------------------------------- /src/snippets/component-counter.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | /components/counter.css 3 | {% endcomment %} 4 | 5 |
6 |
7 | 8 | 9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/snippets/component-image.liquid: -------------------------------------------------------------------------------- 1 |
2 | {{ alt }} 3 |
4 | 5 | {% comment %} 6 |
 7 |   DEBUG image.liquid
 8 | 
 9 |   src:{{ src }}
10 |   file:{{ file }}
11 |   asset:{{ asset }}
12 |   size:{{ size }}
13 |   product:{{ product }}
14 |   _src:{{ _src }}
15 | 
16 | {% endcomment %} 17 | 18 | {% comment %}reset{% endcomment %} 19 | {% assign src = blank %} 20 | {% assign alt = blank %} 21 | {% assign className = blank %} 22 | {% assign fill = false %} 23 | -------------------------------------------------------------------------------- /src/snippets/component-input.liquid: -------------------------------------------------------------------------------- 1 | {% if type == blank %} 2 | {% assign type = 'text' %} 3 | {% endif %} 4 | {% if autocomplete == blank %} 5 | {% assign autocomplete = 'off' %} 6 | {% endif %} 7 | {% if autocapitalize == blank %} 8 | {% assign autocapitalize = 'off' %} 9 | {% endif %} 10 | {% if spellcheck == blank %} 11 | {% assign spellcheck = 'false' %} 12 | {% endif %} 13 | {% if required == blank %} 14 | {% assign required = 'false' %} 15 | {% endif %} 16 | 17 | {%- capture id -%} 18 | {{type}}{{name}}{{ className | replace: ' ', '' }} 19 | {%- endcapture -%} 20 | 21 | {%- capture cx -%} 22 | input-wrapper x 23 | {{ className }} 24 | {%- endcapture -%} 25 | 26 |
27 | 28 | 29 | 30 |
31 | -------------------------------------------------------------------------------- /src/snippets/component-radio.liquid: -------------------------------------------------------------------------------- 1 | 5 | 6 | {% assign label = blank %} 7 | {% assign name = blank %} 8 | {% assign value = blank %} 9 | {% assign attributes = blank %} 10 | {% assign checked = blank %} 11 | -------------------------------------------------------------------------------- /src/snippets/component-select.liquid: -------------------------------------------------------------------------------- 1 | {%- capture uid -%} 2 | select{{name | default: label}} 3 | {%- endcapture -%} 4 | 5 | {%- capture cx -%} 6 | select-wrapper x rel 7 | {{ className }} 8 | {%- endcapture -%} 9 | 10 |
11 | 12 | 13 |
14 | 20 | 21 | chevron icon 22 |
23 |
24 | 25 | {% assign id = blank %} 26 | {% assign style = blank %} 27 | {% assign label = blank %} 28 | {% assign name = blank %} 29 | {% assign required = blank %} 30 | {% assign value = blank %} 31 | {% assign placeholder = blank %} 32 | {% assign options = blank %} 33 | {% assign attributes = blank %} 34 | -------------------------------------------------------------------------------- /src/snippets/component-textarea.liquid: -------------------------------------------------------------------------------- 1 | {% if type == blank %} 2 | {% assign type = 'text' %} 3 | {% endif %} 4 | {% if autocomplete == blank %} 5 | {% assign autocomplete = 'off' %} 6 | {% endif %} 7 | {% if autocapitalize == blank %} 8 | {% assign autocapitalize = 'off' %} 9 | {% endif %} 10 | {% if spellcheck == blank %} 11 | {% assign spellcheck = 'false' %} 12 | {% endif %} 13 | {% if rows == blank %} 14 | {% assign rows = '3' %} 15 | {% endif %} 16 | {% if required == blank %} 17 | {% assign required = 'false' %} 18 | {% endif %} 19 | 20 | {%- capture cx -%} 21 | input-wrapper x 22 | {{ className }} 23 | {%- endcapture -%} 24 | 25 |
26 | 27 | 28 | 29 |
30 | -------------------------------------------------------------------------------- /src/snippets/css-variables.liquid: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/snippets/head-meta.liquid: -------------------------------------------------------------------------------- 1 | {% assign social_type = 'website' %} 2 | {% assign social_title = page_title | strip_html | escape %} 3 | {% assign social_description = page_description | strip_html | escape %} 4 | {% assign social_image = 'social-image.jpg' | file_url %} 5 | 6 | {% if template contains 'product' %} 7 | {% assign social_type = 'product' %} 8 | {% assign social_title = product.title | strip_html | escape %} 9 | {% assign social_price = product.price | money_without_currency | strip_html | escape %} 10 | {% assign social_currency = shop.currency %} 11 | {% if product.featured_image %} 12 | {% assign social_product_img = current_variant.featured_image | default: product.featured_image %} 13 | {% assign social_image = social_product_img | img_url: '1024x' %} 14 | {% endif %} 15 | {% elsif template contains 'collection' %} 16 | {% if collection.description != blank %} 17 | {% assign social_description = collection.description | strip_html | escape %} 18 | {% endif %} 19 | {% if collection.image %} 20 | {% assign social_image = collection.image | img_url: 'master' %} 21 | {% endif %} 22 | {% elsif template contains 'blog' %} 23 | {% for article in blog.articles %} 24 | {% if article.image %} 25 | {% assign social_image = article.image | img_url: 'master' %} 26 | {% break %} 27 | {% endif %} 28 | {% endfor %} 29 | {% elsif template contains 'article' %} 30 | {% assign social_type = 'article' %} 31 | {% assign img_tag = '<' | append: 'img' %} 32 | {% if article.image %} 33 | {% assign social_image = article.image | img_url: 'master' %} 34 | {% elsif article.content contains img_tag %} 35 | {% assign src = article.content | split: 'src="' %} 36 | {% assign src = src[1] | split: '"' | first | remove: 'https:' | remove: 'http:' %} 37 | {% assign social_image = src %} 38 | {% endif %} 39 | {% endif %} 40 | 41 | {% comment %} 42 | Page-level Overrides 43 | 44 | Override variable assignments below this 45 | using `template contains` and similar operators. 46 | {% endcomment %} 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {% if social_price %} 57 | {% endif %} 58 | {% if social_currency %} 59 | {% endif %} 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/snippets/hero.liquid: -------------------------------------------------------------------------------- 1 |
2 |

{{ section.settings.title }}

3 |
-------------------------------------------------------------------------------- /src/snippets/pagination.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/snippets/product-head.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 35 | 36 | 42 | -------------------------------------------------------------------------------- /src/snippets/product-options.liquid: -------------------------------------------------------------------------------- 1 |
2 | {% unless product.has_only_default_variant %} 3 | {% comment %} 4 |
5 |
use selects
6 | {% for option in product.options_with_values %} 7 |
8 | {%- capture options -%} 9 | {% for value in option.values %} 10 | 14 | {% endfor %} 15 | {%- endcapture -%} 16 | {%- capture attributes -%} 17 | data-option-select 18 | data-index='{{ forloop.index0 }}' 19 | {%- endcapture -%} 20 | {% 21 | include 'component-select' with 22 | label: option.name, 23 | name: option.name, 24 | attributes: attributes 25 | options: options 26 | %} 27 |
28 | {% endfor %} 29 |
30 | {% endcomment %} 31 | 32 |
33 |
34 | or radios 35 |
36 | {% for option in product.options_with_values %} 37 |
38 | 39 |
40 | {% for value in option.values %} 41 | {% 42 | include 'component-radio' with 43 | label: value, 44 | value: value, 45 | name: option.name, 46 | checked: option.selected_value == value 47 | %} 48 | {% endfor %} 49 |
50 |
51 | {% endfor %} 52 |
53 | {% endunless %} 54 | 55 | {%- capture options -%} 56 | {% for variant in product.variants %} 57 | 58 | {% endfor %} 59 | {%- endcapture -%} 60 | 61 | {% 62 | include 'component-select' with 63 | name: 'id' 64 | options: options 65 | placeholder: 'Select Option' 66 | className: 'js-variant' 67 | required: true 68 | attributes: 'data-option-main' 69 | %} 70 |
71 | -------------------------------------------------------------------------------- /src/snippets/search.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /src/snippets/social-sharing.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 | Facebook 4 | 5 | 6 | Twitter 7 | 8 | 9 | Pinterest 10 | 11 |
12 | -------------------------------------------------------------------------------- /src/styles/components/buttons.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * /snippets/component-button.liquid 3 | */ 4 | button { 5 | border: 0; 6 | border-radius: 0; 7 | display: inline-block; 8 | cursor: pointer; 9 | -webkit-appearance: none; 10 | } 11 | button.button, 12 | .button[role='button'], 13 | input.button[type='submit'] { 14 | background-color: var(--black); 15 | color: white; 16 | padding: 0.5em 1.5em; 17 | } 18 | -------------------------------------------------------------------------------- /src/styles/components/cart-drawer.scss: -------------------------------------------------------------------------------- 1 | .cart-drawer-outer { 2 | display: none; 3 | 4 | &.is-active { 5 | display: block; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/components/counter.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * /snippets/component-counter.liquid 3 | */ 4 | .counter { 5 | button { 6 | height: 25px; 7 | width: 25px; 8 | background: black; 9 | color: white; 10 | line-height: 23px; 11 | text-align: center; 12 | } 13 | input { 14 | width: 4ch; 15 | padding: 0.3em; 16 | margin: 0 0.5em; 17 | text-align: center; 18 | height: auto; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/components/image.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * /snippets/component-image.liquid 3 | */ 4 | .image { 5 | background: var(--green); 6 | text-align: center; 7 | overflow: hidden; 8 | transition: opacity 200ms var(--cubic); 9 | 10 | &::before { 11 | content: attr(data-alt); 12 | display: block; 13 | position: absolute; 14 | top: 0; 15 | bottom: 0; 16 | left: 0; 17 | right: 0; 18 | margin: auto; 19 | font-size: 0.88rem; 20 | font-family: 'NB International Mono', monospace; 21 | opacity: 0.6; 22 | max-width: 200px; 23 | max-height: 50px; 24 | overflow: hidden; 25 | z-index: 0; 26 | transition: opacity 200ms var(--cubic); 27 | } 28 | 29 | img { 30 | object-fit: cover; 31 | opacity: 0; 32 | transition: opacity 200ms var(--cubic); 33 | } 34 | 35 | &.is-loaded { 36 | &::before { 37 | opacity: 0; 38 | } 39 | img { 40 | opacity: 1; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/styles/global/colors.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --black: #262c4d; 3 | --white: #fff; 4 | --green: #a4e1b9; 5 | --blue: #344fda; 6 | } 7 | 8 | .c-white { 9 | color: var(--white); 10 | } 11 | .bg-white { 12 | background-color: var(--white); 13 | } 14 | 15 | .c-black { 16 | color: var(--black); 17 | } 18 | .bg-black { 19 | background-color: var(--black); 20 | } 21 | 22 | .c-green { 23 | color: var(--green); 24 | } 25 | .bg-green { 26 | background-color: var(--green); 27 | } 28 | 29 | .c-blue { 30 | color: var(--blue); 31 | } 32 | .bg-blue { 33 | background-color: var(--blue); 34 | } 35 | -------------------------------------------------------------------------------- /src/styles/global/containers.scss: -------------------------------------------------------------------------------- 1 | .outer { 2 | padding-left: 1.5em; 3 | padding-right: 1.5em; 4 | @media (--600) { 5 | padding-left: 3em; 6 | padding-right: 3em; 7 | } 8 | } 9 | 10 | .container--s { 11 | max-width: 600px; 12 | } 13 | .container--m { 14 | max-width: 1000px; 15 | } 16 | .container--l { 17 | max-width: 1400px; 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/global/forms.scss: -------------------------------------------------------------------------------- 1 | form { 2 | margin: 0; 3 | } 4 | input, 5 | textarea, 6 | select { 7 | display: block; 8 | outline: 0; 9 | border-radius: 0; 10 | border: 0; 11 | position: relative; 12 | font-size: inherit; 13 | background-color: transparent; 14 | padding: 0.75em 1em; 15 | color: var(--black); 16 | 17 | &:focus { 18 | border-color: var(--black); 19 | } 20 | &::-webkit-input-placeholder { 21 | color: var(--black); 22 | } 23 | /* Change the white to any color ;) */ 24 | &:-webkit-autofill { 25 | -webkit-box-shadow: 0 0 0 30px white inset; 26 | } 27 | } 28 | textarea { 29 | max-width: 100%; 30 | overflow: auto; 31 | resize: vertical; 32 | min-height: 55px; 33 | padding-top: 0.5em; 34 | } 35 | .select-wrapper { 36 | display: block; 37 | position: relative; 38 | 39 | select { 40 | appearance: none; 41 | padding-right: 50px; 42 | width: 100%; 43 | border: 1px solid var(--black); 44 | } 45 | 46 | img { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/global/global.scss: -------------------------------------------------------------------------------- 1 | #root { 2 | transition: opacity 200ms var(--cubic); 3 | } 4 | .is-transitioning #root { 5 | opacity: 0; 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/global/lists.scss: -------------------------------------------------------------------------------- 1 | ol, 2 | ul { 3 | list-style: none; 4 | padding: 0; 5 | margin: 0; 6 | } 7 | ul.list, 8 | ol.list { 9 | padding-left: 2em; 10 | } 11 | ol.list { 12 | list-style: decimal; 13 | } 14 | ul.list { 15 | list-style: disc; 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/global/typography.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto+Mono:400,400i,700,700i'); 2 | 3 | html { 4 | font-size: var(--base-font-size); 5 | } 6 | 7 | body { 8 | color: var(--color-body-text); 9 | font-size: 100%; 10 | font-family: 'Roboto Mono', -apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif; 11 | line-height: 1.7; 12 | font-weight: 400; 13 | -webkit-font-smoothing: antialiased; 14 | } 15 | 16 | /** 17 | * @font-face definitions should be in /assets/fonts.css.liquid 18 | * and linked in of theme.liquid 19 | */ 20 | h1, 21 | h2, 22 | h3, 23 | h4, 24 | h5, 25 | h6 { 26 | font-weight: 400; 27 | } 28 | .h0 { 29 | font-size: calc((54 / 16) * 1rem); 30 | } 31 | .s1, 32 | h1, 33 | .h1 { 34 | font-size: calc((32 / 16) * 1rem); 35 | } 36 | h1, 37 | .h1 { 38 | line-height: 1.1; 39 | } 40 | .s2, 41 | h2, 42 | .h2 { 43 | font-size: calc((24 / 16) * 1rem); 44 | } 45 | h2, 46 | .h2 { 47 | line-height: 1.2; 48 | } 49 | .s3, 50 | h3, 51 | .h3 { 52 | font-size: calc((22 / 16) * 1rem); 53 | } 54 | h3, 55 | .h3 { 56 | line-height: 1.3; 57 | } 58 | .s4, 59 | h4, 60 | .h4 { 61 | font-size: calc((18 / 16) * 1rem); 62 | } 63 | h4, 64 | .h4 { 65 | line-height: 1.4; 66 | } 67 | .s5, 68 | h5, 69 | .h5 { 70 | font-size: calc((16 / 16) * 1rem); 71 | } 72 | h5, 73 | .h5 { 74 | line-height: 1.5; 75 | } 76 | .s6, 77 | h6, 78 | .h6 { 79 | font-size: calc((14 / 16) * 1rem); 80 | } 81 | h6, 82 | .h6 { 83 | line-height: 1.5; 84 | } 85 | .s0, 86 | p, 87 | .p { 88 | font-size: 1rem; 89 | } 90 | p, 91 | .p { 92 | line-height: 1.6; 93 | } 94 | small, 95 | .small { 96 | font-size: calc((12 / 16) * 1rem); 97 | } 98 | .xsmall { 99 | font-size: calc((10 / 16) * 1rem); 100 | } 101 | blockquote { 102 | text-align: center; 103 | color: var(--blue); 104 | padding: 10px; 105 | margin: 0; 106 | p { 107 | font-family: 'freight-display-pro', serif; 108 | font-size: 26px; 109 | line-height: 1.4; 110 | } 111 | } 112 | a { 113 | color: inherit; 114 | text-decoration: none; 115 | } 116 | hr { 117 | display: block; 118 | border: 0; 119 | margin: 0; 120 | height: 1px; 121 | width: 100%; 122 | background-color: currentColor; 123 | color: inherit; 124 | } 125 | strong, 126 | .b { 127 | font-weight: bold; 128 | } 129 | em, 130 | .i { 131 | font-style: italic; 132 | } 133 | .caps { 134 | text-transform: uppercase; 135 | } 136 | .no-under { 137 | text-decoration: none; 138 | } 139 | .light { 140 | font-weight: 300; 141 | } 142 | .book, 143 | .regular, 144 | .medium { 145 | font-weight: 400; 146 | } 147 | .medium { 148 | font-weight: 500; 149 | } 150 | .semi { 151 | font-weight: 600; 152 | } 153 | .bold { 154 | font-weight: 700; 155 | } 156 | .track { 157 | letter-spacing: 0.1em; 158 | } 159 | .track--wide { 160 | letter-spacing: 0.3em; 161 | } 162 | .underline { 163 | position: relative; 164 | overflow: hidden; 165 | &:after { 166 | content: ''; 167 | transition: all 0.25s; 168 | position: absolute; 169 | bottom: 0; 170 | left: 0; 171 | width: 100%; 172 | height: 1px; 173 | border-bottom: 1px solid var(--orange); 174 | } 175 | &--black { 176 | &:after { 177 | bottom: 4px; 178 | border-bottom-color: var(--black); 179 | } 180 | } 181 | &--white { 182 | &:after { 183 | bottom: -2px; 184 | border-bottom-color: white; 185 | border-bottom-width: 2px; 186 | } 187 | &:hover { 188 | &:after { 189 | transform: translateY(4px); 190 | } 191 | } 192 | } 193 | &:hover { 194 | &:after { 195 | transform: translateY(4px); 196 | } 197 | } 198 | } 199 | .link-hover { 200 | transition: all var(--fast) var(--ease); 201 | &:hover { 202 | color: var(--yellow); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/styles/global/var.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --ease: ease-in-out; 3 | --cubic: cubic-bezier(0.12, 0.67, 0.53, 1); 4 | } 5 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import 'node_modules/gridlex/src/gridlex'; 2 | 3 | @import 'global/var'; 4 | @import 'global/colors'; 5 | @import 'global/typography'; 6 | @import 'global/global'; 7 | @import 'global/lists'; 8 | @import 'global/forms'; 9 | @import 'global/containers'; 10 | 11 | @import 'components/buttons'; 12 | @import 'components/image'; 13 | @import 'components/counter'; 14 | @import 'components/cart-drawer'; 15 | 16 | #preview-bar-iframe, 17 | .none { 18 | display: none !important; 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UseAllFive/slater-base/c280af36cb48e8a2ed8cf7346f1dbeac1e139aec/src/styles/templates/.gitkeep -------------------------------------------------------------------------------- /src/templates/404.liquid: -------------------------------------------------------------------------------- 1 |

{{ 'general.404.title' | t }}

2 |

{{ 'general.404.subtext_html' | t }}

3 | -------------------------------------------------------------------------------- /src/templates/article.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | When a comment is submitted, the browser is redirected to a page that includes 3 | the new comment id in its URL. 4 | #comments is a required ID and is used as an anchor link by Shopify. 5 | {% endcomment %} 6 | 7 | {%- assign number_of_comments = article.comments_count -%} 8 | 9 | {% comment %} 10 | If a comment was just submitted but requires moderation, we have an extra comment to count. 11 | {% endcomment %} 12 | {% if comment and comment.status != 'published' %} 13 | {% assign number_of_comments = article.comments_count | plus: 1 %} 14 | {% endif %} 15 | 16 |
17 | 18 | {% if article.image %} 19 | {{ article | img_url: '1024x1024' | img_tag: article.title }} 20 | {% endif %} 21 | 22 |
23 |

{{ article.title }}

24 | {% capture date %} 25 | {{ article.published_at | time_tag: format: 'month_day_year' }} 26 | {% endcapture %} 27 |

{{ 'blogs.article.author_on_date_html' | t: author: article.author, date: date }}

28 |
29 | 30 |
31 | {{ article.content }} 32 |
33 | 34 | {% if article.tags.size > 0 %} 35 | 44 | {% endif %} 45 | 46 | {% if settings.social_sharing_blog %} 47 | {% include 'social-sharing', share_title: article.title, share_permalink: article.url, share_image: article.image %} 48 | {% endif %} 49 | 50 | {% if blog.comments_enabled? %} 51 |

{{ 'blogs.comments.with_count' | t: count: number_of_comments }}

52 | 53 | {% paginate article.comments by 5 %} 54 | 55 |
56 | {% if comment and comment.status and paginate.current_page == 1 %} 57 |

58 | {% if blog.moderated? and comment.status != 'published' %} 59 | {{ 'blogs.comments.success_moderated' | t }} 60 | {% else %} 61 | {{ 'blogs.comments.success' | t }} 62 | {% endif %} 63 |

64 | {% endif %} 65 | 66 | {% if number_of_comments > 0 %} 67 | 96 | 97 | {% if paginate.pages > 1 %} 98 | {% include 'pagination' %} 99 | {% endif %} 100 | {% endif %} 101 |
102 | 103 | {% endpaginate %} 104 | 105 | {% form 'new_comment', article %} 106 |

{{ 'blogs.comments.title' | t }}

107 | 108 | {{ form.errors | default_errors }} 109 | 110 | 113 | 114 | 115 | 118 | 119 | 120 | 123 | 126 | 127 | {% if blog.moderated? %} 128 |

{{ 'blogs.comments.moderated' | t }}

129 | {% endif %} 130 | 131 | 132 | {% endform %} 133 | 134 | {% endif %} 135 | 136 |
137 | -------------------------------------------------------------------------------- /src/templates/blog.liquid: -------------------------------------------------------------------------------- 1 | {% paginate blog.articles by 5 %} 2 | 3 | {%- assign blog_title = blog.title -%} 4 | 5 | {% if current_tags %} 6 | {% capture blog_title %} 7 | {{ blog.title | link_to: blog.url }} 8 | — 9 | {{ current_tags.first }} 10 | {% endcapture %} 11 | {% endif %} 12 | 13 |

{{ blog_title }}

14 | 15 | {% if blog.all_tags.size > 0 %} 16 |

{{ 'blogs.general.categories' | t }}

17 | 18 | 29 | {% endif %} 30 | 31 | {% comment %} 32 | 33 | Article Previews 34 | ==================== 35 | {% endcomment %} 36 | 37 | {% for article in blog.articles %} 38 |

39 | {{ article.title }} 40 |

41 | 42 | {% capture date %} 43 | {{ article.published_at | time_tag: format: 'month_day_year' }} 44 | {% endcapture %} 45 | 46 |

47 | {{ 'blogs.article.author_on_date_html' | t: author: article.author, date: date }} 48 |

49 | 50 | {% if article.image %} 51 | 52 | {{ article | img_url: '1024x1024' | img_tag: article.title }} 53 | 54 | {% endif %} 55 | 56 |
57 | {% if article.excerpt.size > 0 %} 58 | {{ article.excerpt }} 59 | {% else %} 60 |

61 | {{ article.content | strip_html | truncatewords: 100 }} 62 |

63 | {% endif %} 64 |
65 | 66 | {% if blog.comments_enabled? or article.tags.size > 0 %} 67 | 86 | {% endif %} 87 | 88 |

89 | {{ 'blogs.article.read_more' | t }} 90 | → 91 |

92 | 93 | {% endfor %} 94 | 95 | {% if paginate.pages > 1 %} 96 | {% include 'pagination' %} 97 | {% endif %} 98 | 99 | {% endpaginate %} 100 | -------------------------------------------------------------------------------- /src/templates/cart.liquid: -------------------------------------------------------------------------------- 1 |
2 | {% if cart.item_count > 0 %} 3 |

{{ 'cart.general.title' | t }}

4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for item in cart.items %} 18 | 19 | {% comment %} 20 | Cart Item Template 21 | ===================== 22 | The data-label attributes on 27 | 32 | 62 | 72 | 80 | 83 | 84 | 85 | {% endfor %} 86 | 87 |
{{ 'cart.label.product' | t }}{{ 'cart.label.price' | t }}{{ 'cart.label.quantity' | t }}{{ 'cart.label.total' | t }}
elements are mobile-friendly 23 | helpers used for responsive-table labels 24 | {% endcomment %} 25 | 26 |
28 | 29 | {{ item.title | escape }} 30 | 31 | 33 | {{ item.product.title }} 34 | 35 | {% unless item.product.has_only_default_variant %} 36 |

{{ item.variant.title }}

37 | {% endunless %} 38 | 39 |

{{ item.vendor }}

40 | 41 | {%- assign property_size = item.properties | size -%} 42 | 43 | {% if property_size > 0 %} 44 | {% for p in item.properties %} 45 | {% unless p.last == blank %} 46 | {{ p.first }}: 47 | 48 | {% if p.last contains '/uploads/' %} 49 | {{ p.last | split: '/' | last }} 50 | {% else %} 51 | {{ p.last }} 52 | {% endif %} 53 | 54 | {% endunless %} 55 | {% endfor %} 56 | {% endif %} 57 | 58 | 59 | {{ 'cart.general.remove' | t }} 60 | 61 |
63 | {% if item.original_line_price != item.line_price %} 64 | {{ 'cart.label.discounted_price' | t }} 65 | {{ item.price | money }} 66 | {{ 'cart.label.original_price' | t }} 67 | {{ item.original_price | money }} 68 | {% else %} 69 | {{ item.price | money }} 70 | {% endif %} 71 | 73 | 79 | 81 | {{ item.line_price | money }} 82 |
88 | 89 | {% if settings.cart_notes_enable %} 90 | 91 | 92 | {% endif %} 93 | 94 |

{{ 'cart.general.subtotal' | t }}

95 |

{{ cart.total_price | money }}

96 | {% if cart.total_discounts > 0 %} 97 |

{{ 'cart.general.savings' | t }} 98 | {{ cart.total_discounts | money }}

99 | {% endif %} 100 |

{{ 'cart.general.shipping_at_checkout' | t }}

101 | 102 | 103 |
104 | {% else %} 105 |

{{ 'cart.general.title' | t }}

106 | 107 | {% comment %} 108 | Cart empty state 109 | {% endcomment %} 110 |
111 |

{{ 'cart.general.empty' | t }}

112 |

{{ 'cart.general.continue_browsing_html' | t }}

113 |
114 | 115 | {% comment %} 116 | Cart no cookies state 117 | --------------------- 118 | Browser cookies are required to use the cart. If cookies aren't enabled in the 119 | browser a message is displayed prompting the user to enable them. 120 | {% endcomment %} 121 |
122 |

{{ 'cart.general.cookies_required' | t }}

123 |
124 | {% endif %} 125 |
126 | -------------------------------------------------------------------------------- /src/templates/collection.liquid: -------------------------------------------------------------------------------- 1 | {% paginate collection.products by 12 %} 2 | 3 | {% section 'hero-collection' %} 4 | 5 |
6 | 35 | 36 |
37 |
38 |
39 |
    40 | {% for product in collection.products %} 41 | {% assign url = product.url | within: collection %} 42 | {% assign price = product.price | money %} 43 | {% assign comparePrice = product.compare_at_price | money %} 44 | {% assign image = product.featured_image.src | product_img_url: '600x' %} 45 | {% assign alt = product.featured_image.alt | escape %} 46 | 47 |
  • 48 | 49 | {% 50 | include 'component-image' with 51 | src: image, 52 | alt: alt 53 | %} 54 | 55 | 56 |

    {{ product.title }}

    57 |
    58 | 59 |

    60 | {% if comparePrice > price %} 61 | {% if product.price_varies %} 62 | from 63 | {{ price }} 64 | {% else %} 65 | {{ price }} 66 | {{ comparePrice }} 67 | {% endif %} 68 | {% else %} 69 | {{ price }} 70 | {% endif %} 71 | 72 | {% unless product.available %} 73 | sold out 74 | {% endunless %} 75 |

    76 |
  • 77 | {% endfor %} 78 |
79 |
80 |
81 |
82 | 83 | {% if paginate.pages > 1 %} 84 | {{ paginate | default_pagination }} 85 | {% endif %} 86 |
87 | 88 | {% endpaginate %} 89 | -------------------------------------------------------------------------------- /src/templates/customers/account.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% 4 | include 'account-titles' with 5 | title: 'My Account' 6 | %} 7 | 8 | 56 |
57 | 58 |
59 | -------------------------------------------------------------------------------- /src/templates/customers/activate_account.liquid: -------------------------------------------------------------------------------- 1 |

{{ 'customer.activate_account.title' | t }}

2 |

{{ 'customer.activate_account.subtext' | t }}

3 | 4 | {% form 'activate_customer_password' %} 5 | {{ form.errors | default_errors }} 6 | 7 |

8 | 11 | 12 |

13 | 14 |

15 | 18 | 19 |

20 | 21 | 22 | 23 | {% endform %} 24 | -------------------------------------------------------------------------------- /src/templates/customers/addresses.liquid: -------------------------------------------------------------------------------- 1 | {% paginate customer.addresses by 100 %} 2 | 3 |
4 |
5 | {% include 'account-titles' with title: 'Addresses' %} 6 |
7 | 8 | 9 | {% 10 | include 'account-address-form' with 11 | id: 'new', 12 | action: customer.new_address, 13 | title: 'Add New Address', 14 | cta: 'Add Address', 15 | formClassName: 'mb1 pb1' 16 | %} 17 | 18 | {% comment %} 19 | List all customer addresses with a unique edit form. 20 | Also add pagination in case they have a large number of addresses 21 | {% endcomment %} 22 | {% for address in customer.addresses %} 23 | {% if customer.default_address == address %} 24 | {% assign default_addy = true %} 25 | {% else %} 26 | {% assign default_addy = false %} 27 | {% endif %} 28 | {% 29 | include 'account-address' with 30 | address: address, 31 | default: default_addy, 32 | edit: true, 33 | className: 'mb1' 34 | %} 35 | 36 | {% 37 | include 'account-address-form' with 38 | id: address.id, 39 | action: address, 40 | title: 'Edit Address', 41 | cta: 'Update Address', 42 | formClassName: 'is-edit-form mb1' 43 | %} 44 | {% endfor %} 45 | 46 | {% if customer.addresses == empty %} 47 |
You have no saved address.
48 | {% endif %} 49 | 50 | {% include 'pagination' %} 51 |
52 |
53 |
54 | 55 | 86 | 87 | {% endpaginate %} 88 | -------------------------------------------------------------------------------- /src/templates/customers/login.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 | 46 | 47 | 118 |
119 |
120 | -------------------------------------------------------------------------------- /src/templates/customers/order.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | The data-label attributes on elements are mobile-friendly 3 | helpers used for responsive-table labels 4 | {% endcomment %} 5 | 6 |

{{ 'customer.account.title' | t }}

7 | 8 |

{{ 'customer.account.return' | t }}

9 | 10 |

{{ 'customer.order.title' | t: name: order.name }}

11 | 12 |

{{ 'customer.order.date' | t: date: order.created_at | date: "%B %d, %Y %I:%M%p" }}

13 | 14 | {% if order.cancelled %} 15 | {%- assign cancelled_at = order.cancelled_at | date: "%B %d, %Y %I:%M%p" -%} 16 |

{{ 'customer.order.cancelled' | t: date: cancelled_at }}

17 |

{{ 'customer.order.cancelled_reason' | t: reason: order.cancel_reason }}

18 | {% endif %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% for line_item in order.line_items %} 32 | 33 | 45 | 46 | 47 | 48 | 49 | 50 | {% endfor %} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {% for discount in order.discounts %} 59 | 60 | 61 | 62 | 63 | {% endfor %} 64 | 65 | {% for shipping_method in order.shipping_methods %} 66 | 67 | 68 | 69 | 70 | {% endfor %} 71 | 72 | {% for tax_line in order.tax_lines %} 73 | 74 | 75 | 76 | 77 | {% endfor %} 78 | 79 | 80 | 81 | 82 | 83 | 84 |
{{ 'customer.order.product' | t }}{{ 'customer.order.sku' | t }}{{ 'customer.order.price' | t }}{{ 'customer.order.quantity' | t }}{{ 'customer.order.total' | t }}
34 | {{ line_item.title | link_to: line_item.product.url }} 35 | {% if line_item.fulfillment %} 36 |
37 | {%- assign created_at = line_item.fulfillment.created_at | date: format: 'month_day_year' -%} 38 | {{ 'customer.order.fulfilled_at' | t: date: created_at }} 39 | {% if line_item.fulfillment.tracking_number %} 40 | {{ line_item.fulfillment.tracking_company }} #{{ line_item.fulfillment.tracking_number}} 41 | {% endif %} 42 |
43 | {% endif %} 44 |
{{ line_item.sku }}{{ line_item.price | money }}{{ line_item.quantity }}{{ line_item.quantity | times: line_item.price | money }}
{{ 'customer.order.subtotal' | t }}{{ order.subtotal_price | money }}
{{ discount.code }} {{ 'customer.order.discount' | t }}{{ discount.savings | money }}
{{ 'customer.order.shipping' | t }} ({{ shipping_method.title }}){{ shipping_method.price | money }}
{{ 'customer.order.tax' | t }} ({{ tax_line.title }} {{ tax_line.rate | times: 100 }}%){{ tax_line.price | money }}
{{ 'customer.order.total' | t }}{{ order.total_price | money }} {{ order.currency }}
85 | 86 |

{{ 'customer.order.billing_address' | t }}

87 | 88 |

{{ 'customer.order.payment_status' | t }}: {{ order.financial_status_label }}

89 | 90 | {{ order.billing_address | format_address }} 91 | 92 |

{{ 'customer.order.shipping_address' | t }}

93 | 94 |

{{ 'customer.order.fulfillment_status' | t }}: {{ order.fulfillment_status_label }}

95 | 96 | {{ order.shipping_address | format_address }} 97 | -------------------------------------------------------------------------------- /src/templates/customers/register.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 | 63 |
64 |
65 | -------------------------------------------------------------------------------- /src/templates/customers/reset_password.liquid: -------------------------------------------------------------------------------- 1 |
2 | {% form 'reset_customer_password' %} 3 | 4 |

{{ 'customer.reset_password.title' | t }}

5 | 6 |

{{ 'customer.reset_password.subtext' | t: email: email }}

7 | 8 | {{ form.errors | default_errors }} 9 | 10 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | {% endform %} 22 |
23 | -------------------------------------------------------------------------------- /src/templates/gift_card.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | QR code is rendered in `#QrCode` 3 | 4 | `gift_card.pass_url` is true if apple wallet is enabled for the shop 5 | {% endcomment %} 6 | 7 | {% layout 'gift_card' %} 8 | 9 |
10 |
11 | 14 |
15 |
{{ shop.url | escape }}
16 |
17 | 18 |
19 | 20 |

{{ 'gift_cards.issued.subtext' | t }}

21 | {% unless gift_card.enabled %} 22 | {{ 'gift_cards.issued.disabled' | t }} 23 | {% endunless %} 24 | 25 | {%- assign gift_card_expiry_date = gift_card.expires_on | date: "%d/%m/%y" -%} 26 | 27 | {% if gift_card.expired and gift_card.enabled %} 28 | {{ 'gift_cards.issued.expired' | t: expiry: gift_card_expiry_date }} 29 | {% endif %} 30 | 31 | {% if gift_card.expired != true and gift_card.expires_on and gift_card.enabled %} 32 | {{ 'gift_cards.issued.active' | t: expiry: gift_card_expiry_date }} 33 | {% endif %} 34 | 35 | Gift card illustration 36 | 37 | {%- assign initial_value_size = formatted_initial_value | size -%} 38 |

{{ formatted_initial_value }}

39 | 40 | {% if gift_card.balance != gift_card.initial_value %} 41 |

{{ 'gift_cards.issued.remaining_html' | t: balance: gift_card.balance | money }}

42 | {% endif %} 43 | 44 | {%- assign code_size = gift_card.code | format_code | size -%} 45 | {{ gift_card.code | format_code }} 46 | 47 |

{{ 'gift_cards.issued.redeem' | t }}

48 | 49 | {{ 'gift_cards.issued.shop_link' | t }} 50 | 51 |
52 | 53 | {% if gift_card.pass_url %} 54 | 55 | {{ 'gift_cards.issued.add_to_apple_wallet' | t }} 56 | 57 | {% endif %} 58 | 59 | 60 | {{ 'gift_cards.issued.print' | t }} 61 | 62 | 63 |
64 | -------------------------------------------------------------------------------- /src/templates/index.liquid: -------------------------------------------------------------------------------- 1 | {{ content_for_index }} 2 | -------------------------------------------------------------------------------- /src/templates/list-collections.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | This page represents the /collections and /products pages. 3 | {% endcomment %} 4 | 5 |

{{ page_title }}

6 | 7 | {% for collection in collections %} 8 | {% unless collection.handle == 'frontpage' %} 9 | 10 | {% if collection.image != blank %} 11 | {{ collection | img_url: '480x480' | img_tag: collection.title }} 12 | {% elsif collection.products.first != blank %} 13 | {{ collection.products.first | img_url: '480x480' | img_tag: collection.title }} 14 | {% else %} 15 | {% capture current %} 16 | {% cycle 1, 2, 3, 4, 5, 6 %} 17 | {% endcapture %} 18 | {{ 'collection-' | append: current | placeholder_svg_tag: 'placeholder-svg placeholder-svg--small' }} 19 | {% endif %} 20 | 21 | 22 |

23 | {{ collection.title }} 24 |

25 | {% endunless %} 26 | {% endfor %} 27 | -------------------------------------------------------------------------------- /src/templates/page.contact.liquid: -------------------------------------------------------------------------------- 1 |

{{ page.title }}

2 | 3 |
4 | {{ page.content }} 5 |
6 | 7 | {% form 'contact' %} 8 | 9 | {% if form.posted_successfully? %} 10 |

11 | {{ 'contact.form.post_success' | t }} 12 |

13 | {% endif %} 14 | 15 | {{ form.errors | default_errors }} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 33 | 34 | 35 | {% endform %} 36 | -------------------------------------------------------------------------------- /src/templates/page.liquid: -------------------------------------------------------------------------------- 1 |

{{ page.title }}

2 | 3 |
4 | {{ page.content }} 5 |
6 | -------------------------------------------------------------------------------- /src/templates/password.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | The share buttons share the home page URL. The share text is grabbed from the store 3 | meta description. 4 | {% endcomment %} 5 | 6 | {% layout 'password' %} 7 | 8 |

{{ 'general.password_page.opening_soon' | t }}

9 | 10 | {% unless shop.password_message == blank %} 11 |

12 | {{ shop.password_message }} 13 |

14 | {% endunless %} 15 | 16 | {% form 'customer' %} 17 | 18 | {{ form.errors | default_errors }} 19 | 20 | {% if form.posted_successfully? %} 21 | 24 | {% else %} 25 |

{{ 'general.password_page.signup_form_heading' | t }}

26 | 27 | 30 | 31 | 35 | {% endif %} 36 | {% endform %} 37 | 38 | {% if settings.share_facebook or settings.share_twitter %} 39 |

{{ 'general.password_page.spread_the_word' | t }}

40 | {% include 'social-sharing' %} 41 | {% endif %} 42 | 43 | 44 | {% include 'icon-lock' %} 45 | {{ 'general.password_page.password_link' | t }} 46 | → 47 | 48 | -------------------------------------------------------------------------------- /src/templates/product.liquid: -------------------------------------------------------------------------------- 1 |
2 | {%- assign current_variant = product.selected_or_first_available_variant -%} 3 | {%- assign featured_image = current_variant.featured_image | default: product.featured_image -%} 4 | 5 | {% include 'product-head' %} 6 |
7 |
8 |
9 |
10 | 11 |
12 |
    13 | {% for image in product.images %} 14 |
  • 15 | {{ image.alt | escape }} 16 |
  • 17 | {% endfor %} 18 |
19 |
20 | 21 |
22 |

{{ product.title }}

23 | 24 |
25 | {{ product.description }} 26 |
27 | 28 |
29 | {% include 'product-options' %} 30 | 31 |
32 | {% include 'component-counter' %} 33 |
34 | 35 |
36 | {{ current_variant.price | money }} 37 | {{ current_variant.compare_at_price | money }} 38 |
39 | 40 |
41 | {%- capture cta -%} 42 | {% if product.available %} 43 | Add to Cart 44 | {% else %} 45 | Out of Stock 46 | {% endif %} 47 | {%- endcapture -%} 48 | {% 49 | include 'component-button' with 50 | tag: 'button', 51 | name: 'add', 52 | type: 'submit', 53 | className: 'js-add-to-cart', 54 | cta: cta 55 | %} 56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | -------------------------------------------------------------------------------- /src/templates/product.neue.liquid: -------------------------------------------------------------------------------- 1 |
2 | {% assign current_variant = product.selected_or_first_available_variant %} 3 | 4 | {% comment %} {% unless product.has_only_default_variant %}{% endunless %} {% endcomment %} 5 | {% include 'product-head' %} 6 |
7 |
8 |
9 |
10 | 11 |
12 |
    13 | {% for image in product.images %} 14 |
  • 15 | {{ image.alt | escape }} 16 |
  • 17 | {% endfor %} 18 |
19 |
20 |
21 |

{{ product.title }}

22 | 23 |
24 | {{ product.description }} 25 |
26 | 27 |
28 |
29 | {% unless product.has_only_default_variant %} 30 |
31 |
32 | use selects 33 |
34 | {% for option in product.options_with_values %} 35 |
36 | {%- capture options -%} 37 | {% for value in option.values %} 38 | 41 | {% endfor %} 42 | {%- endcapture -%} 43 | {%- capture attributes -%} 44 | data-option-select 45 | data-index='{{ forloop.index0 }}' 46 | {%- endcapture -%} 47 | {% 48 | include 'component-select' with 49 | label: option.name, 50 | name: option.name, 51 | attributes: attributes 52 | options: options 53 | %} 54 |
55 | {% endfor %} 56 |
57 | 58 |
59 |
60 | or radios 61 |
62 | {% for option in product.options_with_values %} 63 |
64 | 65 |
66 | {% for value in option.values %} 67 | {% 68 | include 'component-radio' with 69 | label: value, 70 | value: value, 71 | name: option.name, 72 | checked: option.selected_value == value 73 | %} 74 | {% endfor %} 75 |
76 |
77 | {% endfor %} 78 |
79 | {% endunless %} 80 | 81 | {%- capture options -%} 82 | {% for variant in product.variants %} 83 | 84 | {% endfor %} 85 | {%- endcapture -%} 86 | 87 | {% 88 | include 'component-select' with 89 | name: 'id', 90 | options: options, 91 | attributes: 'data-option-main', 92 | %} 93 |
94 | 95 |
96 | {% include 'component-counter' %} 97 |
98 | 99 |
100 | {{ current_variant.price | money }} 101 | {{ current_variant.compare_at_price | money }} 102 |
103 | 104 |
105 | {%- capture cta -%} 106 | {% if product.available %} 107 | Add to Cart 108 | {% else %} 109 | Out of Stock 110 | {% endif %} 111 | {%- endcapture -%} 112 | {% 113 | include 'component-button' with 114 | tag: 'button', 115 | name: 'add', 116 | type: 'submit', 117 | className: 'js-add-to-cart', 118 | cta: cta 119 | %} 120 |
121 | 122 | 123 |
124 |
125 |
126 |
127 |
128 |
129 | 130 | -------------------------------------------------------------------------------- /src/templates/search.liquid: -------------------------------------------------------------------------------- 1 | {% paginate search.results by 10 %} 2 | 3 |

4 | {% if search.performed %} 5 | {% if search.results_count == 0 %} 6 | {{ 'general.search.no_results_html' | t: terms: search.terms }} 7 | {% else %} 8 | {{ 'general.search.results_for_html' | t: terms: search.terms }} 9 | {% endif %} 10 | {% else %} 11 | {{ 'general.search.title' | t }} 12 | {% endif %} 13 |

14 | 15 |
16 | 19 | 20 | 24 |
25 | 26 | {% if search.performed %} 27 | 69 | 70 | {% if paginate.pages > 1 %} 71 | {% include 'pagination' %} 72 | {% endif %} 73 | {% endif %} 74 | 75 | {% endpaginate %} 76 | --------------------------------------------------------------------------------