├── src ├── base │ ├── index.js │ └── index.scss ├── layout │ ├── index.js │ ├── index.scss │ ├── theme.scss │ └── password.scss ├── sections │ ├── index.js │ ├── index.scss │ └── slideshow.scss ├── snippets │ ├── index.js │ ├── page-header.scss │ ├── address-form.scss │ ├── index.scss │ ├── address-form.js │ ├── footer.scss │ ├── article-item.scss │ ├── product-item.scss │ ├── collection-item.scss │ ├── featured-slider.scss │ └── featured-slider.js ├── templates │ ├── index.js │ ├── blog.scss │ ├── collection.scss │ ├── list-collections.scss │ ├── customers │ │ ├── login.scss │ │ ├── addresses.scss │ │ └── login.js │ ├── product.scss │ ├── password.scss │ ├── article.scss │ ├── index.scss │ ├── gift_card.scss │ ├── search.scss │ ├── search.js │ └── product.js ├── main.scss ├── vendor │ ├── index.js │ └── index.scss └── main.js ├── .theme-check.yml ├── theme ├── locales │ └── en.default.json ├── assets │ └── static-favicon.png ├── templates │ ├── index.liquid │ ├── page.liquid │ ├── 404.liquid │ ├── list-collections.liquid │ ├── blog.liquid │ ├── collection.liquid │ ├── article.liquid │ ├── gift_card.liquid │ ├── password.liquid │ ├── customers │ │ ├── reset_password.liquid │ │ ├── activate_account.liquid │ │ ├── addresses.liquid │ │ ├── register.liquid │ │ ├── account.liquid │ │ ├── login.liquid │ │ └── order.liquid │ ├── page.contact.liquid │ ├── search.liquid │ ├── cart.liquid │ └── product.liquid ├── snippets │ ├── page-header.liquid │ ├── form-message.liquid │ ├── image.liquid │ ├── article-item.liquid │ ├── product-item.liquid │ ├── collection-item.liquid │ ├── footer.liquid │ ├── pagination.liquid │ ├── featured-slider.liquid │ ├── address-form.liquid │ └── main-menu.liquid ├── config │ ├── settings_schema.json │ └── settings_data.json ├── sections │ ├── featured-articles.liquid │ ├── featured-products.liquid │ ├── featured-collections.liquid │ └── slideshow.liquid └── layout │ ├── password.liquid │ └── theme.liquid ├── .vscode ├── settings.json └── extensions.json ├── .gitignore ├── LICENSE ├── webpack.config.js ├── workflow.test.js ├── package.json └── README.md /src/base/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/base/index.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/layout/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sections/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.theme-check.yml: -------------------------------------------------------------------------------- 1 | root: theme 2 | -------------------------------------------------------------------------------- /src/sections/index.scss: -------------------------------------------------------------------------------- 1 | @import 'slideshow'; 2 | -------------------------------------------------------------------------------- /theme/locales/en.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": {} 3 | } 4 | -------------------------------------------------------------------------------- /src/layout/index.scss: -------------------------------------------------------------------------------- 1 | @import './theme'; 2 | @import './password'; 3 | -------------------------------------------------------------------------------- /src/snippets/index.js: -------------------------------------------------------------------------------- 1 | import './address-form'; 2 | import './featured-slider'; 3 | -------------------------------------------------------------------------------- /src/templates/index.js: -------------------------------------------------------------------------------- 1 | import './search'; 2 | import './product'; 3 | import './customers/login'; 4 | -------------------------------------------------------------------------------- /src/snippets/page-header.scss: -------------------------------------------------------------------------------- 1 | .snippet-page-header { 2 | margin-top: -10px; 3 | margin-bottom: 20px; 4 | } 5 | -------------------------------------------------------------------------------- /src/main.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | @import 'layout'; 3 | @import 'snippets'; 4 | @import 'sections'; 5 | @import 'templates'; 6 | -------------------------------------------------------------------------------- /src/snippets/address-form.scss: -------------------------------------------------------------------------------- 1 | .snippet-address-form { 2 | [data-aria-hidden='true'] { 3 | display: none; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /theme/assets/static-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VienDinhCom/bootstrap-shopify-theme/HEAD/theme/assets/static-favicon.png -------------------------------------------------------------------------------- /src/vendor/index.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap'; 2 | import 'smartmenus'; 3 | import 'smartmenus-bootstrap-4'; 4 | 5 | import './index.scss'; 6 | -------------------------------------------------------------------------------- /src/templates/blog.scss: -------------------------------------------------------------------------------- 1 | .template-blog { 2 | .snippet-article-item { 3 | height: calc(100% - 30px); 4 | margin-bottom: 30px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import './base'; 2 | import './layout'; 3 | import './snippets'; 4 | import './sections'; 5 | import './templates'; 6 | 7 | import './main.scss'; 8 | -------------------------------------------------------------------------------- /src/layout/theme.scss: -------------------------------------------------------------------------------- 1 | .layout-theme { 2 | &__content { 3 | min-height: 100vh; 4 | padding-top: 56px + 30px; 5 | padding-bottom: 30px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/templates/collection.scss: -------------------------------------------------------------------------------- 1 | .template-collection { 2 | .snippet-product-item { 3 | height: calc(100% - 30px); 4 | margin-bottom: 30px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/vendor/index.scss: -------------------------------------------------------------------------------- 1 | @import '~bootstrap/scss/bootstrap'; 2 | @import '~smartmenus-bootstrap-4/jquery.smartmenus.bootstrap-4'; 3 | @import '~swiper/swiper-bundle'; 4 | -------------------------------------------------------------------------------- /theme/templates/index.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ content_for_index }} 4 |
5 |
6 | -------------------------------------------------------------------------------- /src/templates/list-collections.scss: -------------------------------------------------------------------------------- 1 | .template-list-collections { 2 | .snippet-collection-item { 3 | height: calc(100% - 30px); 4 | margin-bottom: 30px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/templates/customers/login.scss: -------------------------------------------------------------------------------- 1 | .template-customer-login { 2 | &__tab { 3 | display: none; 4 | 5 | &--active { 6 | display: block; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/templates/product.scss: -------------------------------------------------------------------------------- 1 | .template-product { 2 | &__slider-slide { 3 | > * { 4 | width: 100%; 5 | height: 450px; 6 | object-fit: cover; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/templates/password.scss: -------------------------------------------------------------------------------- 1 | .template-password { 2 | &__message { 3 | margin-bottom: 30px; 4 | } 5 | 6 | &__form { 7 | max-width: 320px; 8 | margin: 0 auto; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "prettier.enable": true, 4 | "editor.formatOnSave": true, 5 | "emmet.triggerExpansionOnTab": true, 6 | "emmet.includeLanguages": { "liquid": "html" } 7 | } 8 | -------------------------------------------------------------------------------- /src/snippets/index.scss: -------------------------------------------------------------------------------- 1 | @import './footer'; 2 | @import './page-header'; 3 | @import './address-form'; 4 | @import './article-item'; 5 | @import './product-item'; 6 | @import './collection-item'; 7 | @import './featured-slider'; 8 | -------------------------------------------------------------------------------- /src/templates/article.scss: -------------------------------------------------------------------------------- 1 | .template-article { 2 | &__image { 3 | width: 100%; 4 | height: 300px; 5 | object-fit: cover; 6 | 7 | @media (max-width: 575px) { 8 | height: 350px; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/templates/customers/addresses.scss: -------------------------------------------------------------------------------- 1 | .template-customer-addresses { 2 | &__card { 3 | height: calc(100% - 30px); 4 | margin-bottom: 30px; 5 | } 6 | 7 | &__card-body > p { 8 | margin-bottom: 0; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "shopify.theme-check-vscode", 4 | "esbenp.prettier-vscode", 5 | "ecmel.vscode-html-css", 6 | "dbaeumer.vscode-eslint", 7 | "stylelint.vscode-stylelint" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /theme/snippets/page-header.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{ title }}

4 |
{{ description }}
5 |
6 |
-------------------------------------------------------------------------------- /src/sections/slideshow.scss: -------------------------------------------------------------------------------- 1 | .section-slideshow { 2 | margin-bottom: 30px; 3 | 4 | &__item-image { 5 | height: 450px; 6 | object-fit: cover; 7 | } 8 | 9 | &__item-caption-text { 10 | text-shadow: 1px 1px 4px rgba($color: #000000, $alpha: 0.8); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/templates/index.scss: -------------------------------------------------------------------------------- 1 | @import 'blog'; 2 | @import 'search'; 3 | @import 'article'; 4 | @import 'product'; 5 | @import 'password'; 6 | @import 'gift_card'; 7 | @import 'collection'; 8 | @import 'list-collections'; 9 | 10 | @import 'customers/login'; 11 | @import 'customers/addresses'; 12 | -------------------------------------------------------------------------------- /src/layout/password.scss: -------------------------------------------------------------------------------- 1 | .layout-password { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | justify-content: center; 6 | align-items: center; 7 | text-align: center; 8 | 9 | &__content { 10 | max-width: 600px; 11 | padding-bottom: 10%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/snippets/address-form.js: -------------------------------------------------------------------------------- 1 | import { withElements } from 'with-elements'; 2 | import { AddressForm } from '@shopify/theme-addresses'; 3 | 4 | withElements('.snippet-address-form', async (formElement) => { 5 | const fields = formElement.querySelector('.snippet-address-form__fields'); 6 | 7 | AddressForm(fields, 'en'); 8 | }); 9 | -------------------------------------------------------------------------------- /theme/templates/page.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

{{ page.title | escape }}

6 |
{{ page.content }}
7 |
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /src/snippets/footer.scss: -------------------------------------------------------------------------------- 1 | .snippet-footer { 2 | padding: 20px 0 25px 0; 3 | 4 | &__nav { 5 | display: flex; 6 | justify-content: center; 7 | flex-wrap: wrap; 8 | } 9 | 10 | &__nav-link { 11 | display: block; 12 | margin: 3px 10px; 13 | } 14 | 15 | &__copyright { 16 | font-size: 13px; 17 | text-align: center; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /theme/config/settings_schema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "theme_info", 4 | "theme_name": "Bootstrap", 5 | "theme_version": "3.0.0", 6 | "theme_author": "Vien Dinh", 7 | "theme_documentation_url": "https://github.com/maxvien/bootstrap-shopify-theme", 8 | "theme_support_url": "https://github.com/maxvien/bootstrap-shopify-theme/issues" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /src/templates/gift_card.scss: -------------------------------------------------------------------------------- 1 | .template-gift-card { 2 | display: flex; 3 | justify-content: center; 4 | 5 | &__card { 6 | width: 320px; 7 | 8 | @media (max-width: 320px) { 9 | width: 100%; 10 | } 11 | } 12 | 13 | &__image { 14 | padding: 1rem 1rem 0 1rem; 15 | width: 100%; 16 | height: 220px; 17 | object-fit: cover; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # development 12 | 13 | # assets 14 | theme/assets/* 15 | !theme/assets/static-* 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* -------------------------------------------------------------------------------- /src/snippets/article-item.scss: -------------------------------------------------------------------------------- 1 | .snippet-article-item { 2 | height: 100%; 3 | 4 | &__image { 5 | width: 100%; 6 | height: 200px; 7 | object-fit: cover; 8 | 9 | @media (max-width: 575px) { 10 | height: 350px; 11 | } 12 | } 13 | 14 | &__card-body { 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | 19 | &__card-info { 20 | flex: 1; 21 | margin-bottom: 0.75rem; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/snippets/product-item.scss: -------------------------------------------------------------------------------- 1 | .snippet-product-item { 2 | height: 100%; 3 | 4 | &__image { 5 | width: 100%; 6 | height: 200px; 7 | object-fit: cover; 8 | 9 | @media (max-width: 575px) { 10 | height: 350px; 11 | } 12 | } 13 | 14 | &__card-body { 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | 19 | &__card-info { 20 | flex: 1; 21 | margin-bottom: 0.75rem; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/snippets/collection-item.scss: -------------------------------------------------------------------------------- 1 | .snippet-collection-item { 2 | height: 100%; 3 | 4 | &__image { 5 | width: 100%; 6 | height: 200px; 7 | object-fit: cover; 8 | 9 | @media (max-width: 575px) { 10 | height: 350px; 11 | } 12 | } 13 | 14 | &__card-body { 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | 19 | &__card-info { 20 | flex: 1; 21 | margin-bottom: 0.75rem; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /theme/templates/404.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 404 6 |
The page you are looking for was not found.
7 | Back to Home 8 |
9 |
10 |
11 |
-------------------------------------------------------------------------------- /theme/snippets/form-message.liquid: -------------------------------------------------------------------------------- 1 |
2 | {% if form.posted_successfully? %} 3 | 6 | {% else %} 7 | {% for field in form.errors %} 8 | 11 | {% endfor %} 12 | {% endif %} 13 |
14 | 15 | -------------------------------------------------------------------------------- /theme/templates/list-collections.liquid: -------------------------------------------------------------------------------- 1 | {% render 'page-header' title: 'Collections' %} 2 | 3 |
4 |
5 |
6 | {% for collection in collections %} 7 |
8 | {% render 'collection-item' collection: collection %} 9 |
10 | {% endfor %} 11 |
12 |
13 |
14 | 15 | -------------------------------------------------------------------------------- /theme/templates/blog.liquid: -------------------------------------------------------------------------------- 1 | {% render 'page-header' title: page_title %} 2 | 3 |
4 |
5 | {%- paginate blog.articles by 12 -%} 6 |
7 | {%- for article in blog.articles -%} 8 |
9 | {% render 'article-item' article: article %} 10 |
11 | {% endfor %} 12 |
13 | {% render 'pagination' paginate: paginate %} 14 | {%- endpaginate -%} 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/snippets/featured-slider.scss: -------------------------------------------------------------------------------- 1 | .snippet-featured-slider { 2 | margin-bottom: 30px; 3 | 4 | &__swiper-slide { 5 | height: auto; 6 | } 7 | 8 | &__swiper-control { 9 | width: 20%; 10 | transition-duration: 0.3s; 11 | 12 | @media (min-width: 576px) { 13 | width: 15%; 14 | } 15 | 16 | @media (min-width: 768px) { 17 | width: 10%; 18 | } 19 | 20 | @media (min-width: 992px) { 21 | width: 5%; 22 | } 23 | } 24 | 25 | &:hover { 26 | .snippet-featured-slider__swiper-control { 27 | background: rgba($color: #000000, $alpha: 0.1); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /theme/snippets/image.liquid: -------------------------------------------------------------------------------- 1 | {%- liquid 2 | if width 3 | assign img_width = width 4 | else 5 | assign img_width = image.width 6 | endif 7 | 8 | if height 9 | assign img_height = height 10 | else 11 | assign img_height = image.height 12 | endif 13 | 14 | if alt 15 | assign img_alt = alt 16 | else 17 | assign img_alt = image.alt 18 | endif 19 | -%} 20 | 21 | {% assign size = img_width | append: 'x' | append: img_height %} 22 | 23 | {{ img_alt | escape }} -------------------------------------------------------------------------------- /theme/templates/collection.liquid: -------------------------------------------------------------------------------- 1 | {% render 'page-header' title: collection.title description: collection.description %} 2 | 3 |
4 |
5 | {%- paginate collection.products by 12 -%} 6 |
7 | {%- for product in collection.products -%} 8 |
9 | {% render 'product-item' product: product %} 10 |
11 | {% endfor %} 12 |
13 | {% render 'pagination' paginate: paginate %} 14 | {%- endpaginate -%} 15 |
16 |
17 | -------------------------------------------------------------------------------- /theme/templates/article.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | {% render 'image' class: 'template-article__image card-img-top', image: article.image, width: 1024, height: 1024 %} 6 | 7 |
8 |

{{ article.title | escape }}

9 | 10 |
11 | Published at 12 | {{ article.published_at | date: format: 'abbreviated_date' }} 13 |
14 | 15 |
{{ article.content }}
16 |
17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /src/templates/customers/login.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | $('#show-main-tab').on('click', () => { 4 | $('.template-customer-login__tab').each((index, tab) => { 5 | $(tab).removeClass('template-customer-login__tab--active'); 6 | if ($(tab).hasClass('template-customer-login__tab--main')) { 7 | $(tab).addClass('template-customer-login__tab--active'); 8 | } 9 | }); 10 | }); 11 | 12 | $('#show-recovery-tab').on('click', () => { 13 | $('.template-customer-login__tab').each((index, tab) => { 14 | $(tab).removeClass('template-customer-login__tab--active'); 15 | if ($(tab).hasClass('template-customer-login__tab--recovery')) { 16 | $(tab).addClass('template-customer-login__tab--active'); 17 | } 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/templates/search.scss: -------------------------------------------------------------------------------- 1 | .template-search { 2 | &__card { 3 | align-items: stretch; 4 | 5 | @media (min-width: 768px) { 6 | flex-direction: row; 7 | } 8 | } 9 | 10 | &__card-image { 11 | width: 100%; 12 | height: 300px; 13 | 14 | img { 15 | width: 100%; 16 | height: 100%; 17 | object-fit: cover; 18 | } 19 | 20 | @media (min-width: 768px) { 21 | width: 150px; 22 | height: 100%; 23 | } 24 | } 25 | 26 | &__card-body { 27 | flex: 1; 28 | } 29 | 30 | &__form { 31 | position: relative; 32 | } 33 | 34 | &__form-results { 35 | position: absolute; 36 | top: 100%; 37 | width: 100%; 38 | margin-top: 10px; 39 | z-index: 1000; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /theme/sections/featured-articles.liquid: -------------------------------------------------------------------------------- 1 | {% render 'featured-slider' section: section, type: 'article' %} 2 | 3 | {% schema %} 4 | 5 | { 6 | "name": "Featured Articles", 7 | "settings": [ 8 | { 9 | "id": "title", 10 | "type": "text", 11 | "label": "Title", 12 | "default": "Featured Articles" 13 | } 14 | ], 15 | "blocks": [ 16 | { 17 | "type": "item", 18 | "name": "Item", 19 | "settings": [ 20 | { 21 | "id": "article", 22 | "type": "article", 23 | "label": "Article" 24 | } 25 | ] 26 | } 27 | ], 28 | "presets": [ 29 | { 30 | "category": "Sliders", 31 | "name": "Featured Articles" 32 | } 33 | ] 34 | } 35 | 36 | {% endschema %} -------------------------------------------------------------------------------- /theme/sections/featured-products.liquid: -------------------------------------------------------------------------------- 1 | {% render 'featured-slider' section: section, type: 'product' %} 2 | 3 | 4 | {% schema %} 5 | 6 | { 7 | "name": "Featured Products", 8 | "settings": [ 9 | { 10 | "id": "title", 11 | "type": "text", 12 | "label": "Title", 13 | "default": "Featured Products" 14 | } 15 | ], 16 | "blocks": [ 17 | { 18 | "type": "item", 19 | "name": "Item", 20 | "settings": [ 21 | { 22 | "id": "product", 23 | "type": "product", 24 | "label": "Product" 25 | } 26 | ] 27 | } 28 | ], 29 | "presets": [ 30 | { 31 | "category": "Sliders", 32 | "name": "Featured Products" 33 | } 34 | ] 35 | } 36 | 37 | {% endschema %} -------------------------------------------------------------------------------- /theme/sections/featured-collections.liquid: -------------------------------------------------------------------------------- 1 | {% render 'featured-slider' section: section, type: 'collection' %} 2 | 3 | {% schema %} 4 | 5 | { 6 | "name": "Featured Collections", 7 | "settings": [ 8 | { 9 | "id": "title", 10 | "type": "text", 11 | "label": "Title", 12 | "default": "Featured Collections" 13 | } 14 | ], 15 | "blocks": [ 16 | { 17 | "type": "item", 18 | "name": "Item", 19 | "settings": [ 20 | { 21 | "id": "collection", 22 | "type": "collection", 23 | "label": "Collection" 24 | } 25 | ] 26 | } 27 | ], 28 | "presets": [ 29 | { 30 | "category": "Sliders", 31 | "name": "Featured Collections" 32 | } 33 | ] 34 | } 35 | 36 | {% endschema %} -------------------------------------------------------------------------------- /theme/snippets/article-item.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 | {% render 'image' class: 'snippet-article-item__image card-img-top', image: article.image, alt: article.title, width: 450, height: 450 %} 4 | 5 |
6 |
7 |
{{ article.title }}
8 |
9 | {{ article.published_at | date: format: 'abbreviated_date' }} 10 |
11 |

12 | {%- if article.excerpt.size > 0 -%} 13 | {{ article.excerpt }} 14 | {%- else -%} 15 | {{ article.content | strip_html | truncate: 120 }} 16 | {%- endif -%} 17 |

18 |
19 | 20 | Read 21 |
22 |
23 | -------------------------------------------------------------------------------- /theme/templates/gift_card.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | {{ gift_card.balance | money }} 7 |
8 | 9 |

10 | Hey, 11 | {{ gift_card.customer.first_name }}! Use this code at checkout to redeem your gift card. 12 |

13 | 14 |

15 | 16 |

17 | 18 | {% if gift_card.expires_on %} 19 |

This gift card expires on 20 | {{ gift_card.expires_on }}

21 | {% endif %} 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /theme/snippets/product-item.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 | {% render 'image' class: 'snippet-product-item__image card-img-top', image: product.featured_image, alt: product.title, width: 450, height: 450 %} 4 | 5 |
6 |
7 |
{{ product.title }}
8 |

{{ product.description | strip_html | truncate: 50 }}

9 | 10 |
11 |
12 | {%- if product.price_varies -%} 13 | Starting at 14 | {{ product.price_min | money_without_trailing_zeros }} 15 | 16 | {%- else -%} 17 | {{ product.price | money_without_trailing_zeros }} 18 | {%- endif -%} 19 |
20 | 21 | View 22 |
23 |
24 | -------------------------------------------------------------------------------- /theme/snippets/collection-item.liquid: -------------------------------------------------------------------------------- 1 |
2 | 3 | {% if collection.image %} 4 | {% render 'image' class: 'snippet-collection-item__image card-img-top', image: collection.image, width: 450, height: 450 %} 5 | {% else %} 6 | {% render 'image' class: 'snippet-collection-item__image card-img-top', image: collection.products.first.featured_image, alt: collection.title, width: 450, height: 450 %} 7 | {% endif %} 8 | 9 |
10 |
11 |
{{ collection.title }}
12 |

{{ collection.description | strip_html | truncate: 100 }}

13 |
14 | 15 | Explore 16 | {{ collection.products_count }} 17 | {{ collection.products_count | pluralize: 'Product', 'Products' }} 18 | 19 |
20 |
21 | -------------------------------------------------------------------------------- /theme/snippets/footer.liquid: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /theme/layout/password.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ page_title }} 10 | 11 | 12 | 13 | 14 | 15 | {{ content_for_header }} 16 | 17 | 18 |
19 | {{ content_for_layout }} 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/snippets/featured-slider.js: -------------------------------------------------------------------------------- 1 | import Swiper from 'swiper'; 2 | import { withElements } from 'with-elements'; 3 | 4 | withElements('.snippet-featured-slider', async (sliderElement) => { 5 | const containerElement = sliderElement.querySelector('.swiper-container'); 6 | const prevBtn = containerElement.querySelector('.carousel-control-prev'); 7 | const nextBtn = containerElement.querySelector('.carousel-control-next'); 8 | 9 | const swiper = new Swiper(containerElement, { 10 | loop: true, 11 | 12 | slidesPerView: 1, 13 | spaceBetween: 0, 14 | breakpoints: { 15 | 576: { 16 | slidesPerView: 2, 17 | spaceBetween: 30, 18 | }, 19 | 768: { 20 | slidesPerView: 3, 21 | spaceBetween: 30, 22 | }, 23 | 992: { 24 | slidesPerView: 4, 25 | spaceBetween: 30, 26 | }, 27 | }, 28 | }); 29 | 30 | prevBtn.addEventListener('click', () => { 31 | swiper.slidePrev(); 32 | }); 33 | 34 | nextBtn.addEventListener('click', () => { 35 | swiper.slideNext(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vien Dinh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /theme/templates/password.liquid: -------------------------------------------------------------------------------- 1 | {% layout 'password' %} 2 | 3 |
4 |
5 |

Opening Soon

6 | 7 | {% if shop.password_message != blank %} 8 |

{{ shop.password_message }}

9 | {% else %} 10 |

11 | This store is password protected. 12 | Use the password to enter the store. 13 |

14 | {% endif %} 15 |
16 | 17 |
18 | {% form 'storefront_password' %} 19 | 20 | {% render 'form-message' form: form %} 21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | Are you the store owner? 29 | Log in here 30 |
31 | {% endform %} 32 |
33 |
34 | 35 | -------------------------------------------------------------------------------- /theme/layout/theme.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{ page_title }} 12 | 13 | 14 | 15 | 16 | 17 | {{ content_for_header }} 18 | 19 | 20 | {% render 'main-menu' %} 21 | 22 |
23 | {{ content_for_layout }} 24 |
25 | 26 | {% render 'footer' %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const cp = require('child_process'); 3 | const Encore = require('@symfony/webpack-encore'); 4 | const postcssPresetEnv = require('postcss-preset-env'); 5 | 6 | const src = path.resolve('src'); 7 | const dest = path.resolve('theme/assets'); 8 | 9 | Encore 10 | // sources 11 | .addEntry('main', path.join(src, 'main.js')) 12 | .addEntry('vendor', path.join(src, 'vendor/index.js')) 13 | 14 | // destination 15 | .setOutputPath(dest) 16 | .setPublicPath('/') 17 | 18 | // features 19 | .enableSassLoader() 20 | .autoProvidejQuery() 21 | .disableSingleRuntimeChunk() 22 | .enableSourceMaps(Encore.isDev()) 23 | .enablePostCssLoader((options) => { 24 | options.postcssOptions = { 25 | plugins: [postcssPresetEnv()], 26 | }; 27 | }) 28 | .configureBabelPresetEnv((config) => { 29 | config.useBuiltIns = 'usage'; 30 | config.corejs = 3; 31 | }) 32 | .configureImageRule({ 33 | filename: '[name].[hash:8][ext]', 34 | }) 35 | .configureFontRule({ 36 | filename: '[name].[hash:8][ext]', 37 | }) 38 | .cleanupOutputBeforeBuild([], () => { 39 | cp.spawnSync('git', ['clean', '-xdf', dest], { stdio: 'inherit' }); 40 | }); 41 | 42 | module.exports = Encore.getWebpackConfig(); 43 | -------------------------------------------------------------------------------- /theme/templates/customers/reset_password.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Reset Password

6 |
7 | {% form 'reset_customer_password' %} 8 | {% render 'form-message' form: form %} 9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 | 20 | 21 | {% endform %} 22 |
23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /theme/templates/customers/activate_account.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Activate Account

6 |
7 | {% form 'activate_customer_password' %} 8 | {% render 'form-message' form: form %} 9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 | 20 | 21 | 22 | {% endform %} 23 |
24 |
25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /theme/snippets/pagination.liquid: -------------------------------------------------------------------------------- 1 | {%- if paginate.pages > 1 -%} 2 | 44 | {%- endif -%} -------------------------------------------------------------------------------- /theme/templates/customers/addresses.liquid: -------------------------------------------------------------------------------- 1 | {% render 'page-header' title: page_title %} 2 | 3 |
4 |
5 |
6 | {% for address in customer.addresses %} 7 |
8 |
9 |
10 | {% if address == customer.default_address %} 11 |
DEFAULT
12 | {% endif %} 13 | 14 | {{ address | format_address }} 15 |
16 | 27 |
28 |
29 | {% endfor %} 30 |
31 | 34 | {% render 'address-form' address: customer.new_address %} 35 |
36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /theme/snippets/featured-slider.liquid: -------------------------------------------------------------------------------- 1 | 40 | -------------------------------------------------------------------------------- /theme/templates/customers/register.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Register

6 |
7 | {% form 'create_customer' %} 8 | {% render 'form-message' form: form %} 9 | 10 |
11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 | 20 | 21 |
22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 |
30 | 31 | 32 | {% endform %} 33 |
34 |
35 |
36 |
37 |
38 | -------------------------------------------------------------------------------- /src/templates/search.js: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash/debounce'; 2 | import { withElements } from 'with-elements'; 3 | import PredictiveSearch from '@shopify/theme-predictive-search'; 4 | 5 | withElements('.template-search', async (templateElement) => { 6 | const formElement = templateElement.querySelector('.template-search__form'); 7 | const formInputElement = formElement.querySelector('.template-search__form-input'); 8 | const formResultsElement = formElement.querySelector('.template-search__form-results'); 9 | 10 | var predictiveSearch = new PredictiveSearch({ 11 | search_path: PredictiveSearch.SEARCH_PATH, 12 | resources: { 13 | type: [PredictiveSearch.TYPES.PAGE, PredictiveSearch.TYPES.ARTICLE, PredictiveSearch.TYPES.PRODUCT], 14 | limit: 2, 15 | }, 16 | }); 17 | 18 | formInputElement.onkeyup = debounce(({ target: { value } }) => { 19 | if (value === '') { 20 | formResultsElement.innerHTML = ''; 21 | } else { 22 | predictiveSearch.query(value); 23 | } 24 | }, 500); 25 | 26 | window.onclick = (event) => { 27 | if (!formResultsElement.contains(event.target)) { 28 | formResultsElement.innerHTML = ''; 29 | } 30 | }; 31 | 32 | formResultsElement.onfocusout = () => { 33 | formResultsElement.innerHTML = ''; 34 | }; 35 | 36 | predictiveSearch.on('success', function ({ resources: { results } }) { 37 | const items = []; 38 | 39 | for (const itemType in results) { 40 | if (Object.hasOwnProperty.call(results, itemType)) { 41 | results[itemType].forEach((item) => { 42 | items.push({ itemType, ...item }); 43 | }); 44 | } 45 | } 46 | 47 | formResultsElement.innerHTML = items 48 | .map((item) => { 49 | return `${item.title}`; 50 | }) 51 | .join(''); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /workflow.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const glob = require('glob'); 3 | const path = require('path'); 4 | 5 | const themeDir = 'theme'; 6 | const sourceDir = 'src'; 7 | 8 | describe('files', () => { 9 | const moduleDirs = ['layout', 'snippets', 'sections', 'templates']; 10 | 11 | async function matchFiles(pattern) { 12 | return new Promise((resolve, reject) => { 13 | glob(pattern, null, (error, files) => { 14 | if (error) reject(error); 15 | resolve(files); 16 | }); 17 | }); 18 | } 19 | 20 | async function checkLiquidFiles(extension) { 21 | const patterns = moduleDirs.map((folder) => path.join(sourceDir, folder, `**/*.${extension}`)); 22 | 23 | for (const pattern of patterns) { 24 | const files = await matchFiles(pattern); 25 | 26 | const liquidFiles = files.map((file) => { 27 | return themeDir + file.replace(sourceDir, '').replace(`.${extension}`, '.liquid'); 28 | }); 29 | 30 | files.forEach((file, index) => { 31 | const liquidFile = liquidFiles[index]; 32 | const isNotExisting = !fs.existsSync(liquidFile); 33 | const isNotIndex = path.basename(file).indexOf('index.') < 0; 34 | 35 | if (isNotExisting && isNotIndex) { 36 | throw Error(`The ${file} must have its own ${liquidFile}`); 37 | } 38 | }); 39 | } 40 | } 41 | 42 | test('each style file should have its own liquid file', async () => { 43 | const extensions = ['css', 'scss']; 44 | 45 | for (const extension of extensions) { 46 | await checkLiquidFiles(extension); 47 | } 48 | }); 49 | 50 | test('each script file should have its own liquid file', async () => { 51 | const extensions = ['js', 'mjs', 'jsx', 'vue']; 52 | 53 | for (const extension of extensions) { 54 | await checkLiquidFiles(extension); 55 | } 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /theme/templates/page.contact.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

{{ page.title | escape }}

6 |
{{ page.content }}
7 | 8 |
9 |
10 | {% form 'contact' %} 11 | {% render 'form-message' form: form %} 12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 |
32 | 33 | 34 | {% endform %} 35 |
36 |
37 |
38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /theme/templates/customers/account.liquid: -------------------------------------------------------------------------------- 1 | {% render 'page-header' title: page_title %} 2 | 3 |
4 |
5 | {% paginate customer.orders by 10 %} 6 | {% if customer.orders.size == 0 %} 7 |

You haven't placed any orders yet.

8 | {% else %} 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% for order in customer.orders %} 22 | 23 | 26 | 29 | 32 | 35 | 38 | 39 | {% endfor %} 40 | 41 |
OrderDatePaymentFulfillmentTotal
24 | {{ order.name }} 25 | 27 | {{ order.created_at | time_tag: format: 'date' }} 28 | 30 | {{ order.financial_status_label }} 31 | 33 | {{ order.fulfillment_status_label }} 34 | 36 | {{ order.total_price | money }} 37 |
42 |
43 | {% endif %} 44 | 45 | {% render 'pagination' paginate: paginate %} 46 | {% endpaginate %} 47 | 48 | 53 | 54 |
55 |
56 | 57 | -------------------------------------------------------------------------------- /theme/templates/customers/login.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Login

6 |
7 | 26 | 27 | 41 |
42 |
43 |
44 |
45 |
46 | 47 | -------------------------------------------------------------------------------- /theme/templates/search.liquid: -------------------------------------------------------------------------------- 1 | {% render 'page-header' title: 'Search' %} 2 | 3 | 60 | -------------------------------------------------------------------------------- /theme/templates/customers/order.liquid: -------------------------------------------------------------------------------- 1 | {% render 'page-header' title: page_title %} 2 | 3 |
4 |
5 | {% paginate customer.orders by 10 %} 6 | {% if customer.orders.size == 0 %} 7 |

You haven't placed any orders yet.

8 | {% else %} 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for item in order.line_items %} 21 | 22 | 25 | 28 | 31 | 34 | 35 | {% endfor %} 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 56 | 57 | 60 | 61 | 62 | 63 | 64 | 65 | 68 | 69 | 70 |
ItemSKUQuantityPrice
23 | {{ item.title }} 24 | 26 | {{ item.sku }} 27 | 29 | {{ item.quantity }} 30 | 32 | {{ item.original_price | money }} 33 |
Subtotal 42 | {{ order.subtotal_price | money }} 43 |
International Shipping 50 | {{ order.shipping_price | money }} 51 |
Tax 58 | {{ order.tax_price | money }} 59 |
Total 66 | {{ order.total_price | money }} 67 |
71 |
72 | {% endif %} 73 | 74 | {% render 'pagination' paginate: paginate %} 75 | {% endpaginate %} 76 | 77 |
78 |
79 |

Billing Address

80 | {{ order.billing_address | format_address }} 81 |
82 |
83 |

Shipping Address

84 | {{ order.shipping_address | format_address }} 85 |
86 |
87 |
88 |
89 | 90 | -------------------------------------------------------------------------------- /theme/sections/slideshow.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 | 42 |
43 |
44 | 45 | 46 | {% schema %} 47 | 48 | { 49 | "name": "Slideshow", 50 | "blocks": [ 51 | { 52 | "type": "item", 53 | "name": "Item", 54 | "settings": [ 55 | { 56 | "id": "title", 57 | "type": "text", 58 | "label": "Title" 59 | }, { 60 | "id": "description", 61 | "type": "richtext", 62 | "label": "Description" 63 | }, { 64 | "id": "actionButton", 65 | "type": "text", 66 | "label": "Action Button" 67 | }, { 68 | "id": "actionLink", 69 | "type": "url", 70 | "label": "Action Link" 71 | }, { 72 | "id": "image", 73 | "type": "image_picker", 74 | "label": "Image" 75 | } 76 | ] 77 | } 78 | ], 79 | "presets": [ 80 | { 81 | "category": "Sliders", 82 | "name": "Slideshow" 83 | } 84 | ] 85 | } 86 | 87 | {% endschema %} 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-shopify-theme", 3 | "version": "3.0.0", 4 | "author": "Vien Dinh ", 5 | "repository": "maxvien/bootstrap-shopify-theme", 6 | "license": "MIT", 7 | "volta": { 8 | "node": "18.20.7", 9 | "npm": "10.8.2" 10 | }, 11 | "scripts": { 12 | "serve": "run-p serve:*", 13 | "serve:build": "encore dev --watch", 14 | "serve:theme": "cd theme && shopify theme serve", 15 | "pull": "run-s -c pull:before pull:theme pull:after", 16 | "pull:before": "git add .", 17 | "pull:theme": "cd theme && shopify theme pull -n", 18 | "pull:after": "git clean -xdf theme/assets", 19 | "push": "run-s push:build push:theme", 20 | "push:build": "encore production", 21 | "push:theme": "cd theme && shopify theme push", 22 | "test": "jest", 23 | "lint": "run-s lint:*", 24 | "lint:theme": "shopify theme check", 25 | "lint:scripts": "eslint src/**/*.js", 26 | "lint:styles": "stylelint src/**/*.{css,scss}", 27 | "lint:format": "prettier --check .", 28 | "fix": "run-s -c fix:*", 29 | "fix:theme": "shopify theme check -a", 30 | "fix:scripts": "eslint --fix src/**/*.js", 31 | "fix:styles": "stylelint --fix src/**/*.{css,scss}", 32 | "fix:format": "prettier --write ." 33 | }, 34 | "dependencies": { 35 | "@popperjs/core": "^2.9.2", 36 | "@shopify/theme-addresses": "^4.1.1", 37 | "@shopify/theme-predictive-search": "^4.1.1", 38 | "@shopify/theme-product": "^4.1.0", 39 | "@shopify/theme-product-form": "^4.1.1", 40 | "bootstrap": "^5.0.2", 41 | "jquery": "^3.6.0", 42 | "lodash": "^4.17.21", 43 | "smartmenus": "^1.1.1", 44 | "smartmenus-bootstrap-4": "^0.1.0", 45 | "swiper": "^6.8.0", 46 | "with-elements": "^1.0.0" 47 | }, 48 | "devDependencies": { 49 | "@namics/stylelint-bem": "^6.3.4", 50 | "@symfony/webpack-encore": "^1.5.0", 51 | "core-js": "^3.16.1", 52 | "eslint": "^7.32.0", 53 | "jest": "^27.0.6", 54 | "npm-run-all": "^4.1.5", 55 | "postcss-loader": "^5.0.0", 56 | "postcss-preset-env": "^6.7.0", 57 | "prettier": "^2.3.2", 58 | "sass": "^1.37.5", 59 | "sass-loader": "^12.0.0", 60 | "stylelint": "^13.13.1" 61 | }, 62 | "browserslist": { 63 | "production": [ 64 | ">0.2%", 65 | "not dead", 66 | "not op_mini all" 67 | ], 68 | "development": [ 69 | "last 1 chrome version", 70 | "last 1 firefox version", 71 | "last 1 safari version" 72 | ] 73 | }, 74 | "prettier": { 75 | "tabWidth": 2, 76 | "semi": true, 77 | "printWidth": 120, 78 | "singleQuote": true, 79 | "trailingComma": "es5" 80 | }, 81 | "eslintConfig": { 82 | "extends": [ 83 | "eslint:recommended" 84 | ], 85 | "env": { 86 | "es6": true, 87 | "jest": true, 88 | "node": true, 89 | "browser": true 90 | }, 91 | "parserOptions": { 92 | "sourceType": "module", 93 | "ecmaVersion": "latest" 94 | } 95 | }, 96 | "stylelint": { 97 | "plugins": [ 98 | "@namics/stylelint-bem" 99 | ], 100 | "rules": { 101 | "max-nesting-depth": 2, 102 | "plugin/stylelint-bem-namics": { 103 | "patternPrefixes": [ 104 | "layout", 105 | "snippet", 106 | "section", 107 | "template" 108 | ] 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /theme/templates/cart.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |

Cart

6 |
7 |
8 | {%- if cart.item_count > 0 -%} 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {%- for item in cart.items -%} 24 | 25 | 28 | 31 | 34 | 37 | 42 | 50 | 51 | {%- endfor -%} 52 | 53 |
#ProductVariantQuantityPrice
26 | {% render 'image' image: item.image, width: 50, height: 50 %} 27 | 29 | {{ item.product.title }} 30 | 32 | {{ item.variant.title }} 33 | 35 | 36 | 38 | 39 | {{ item.final_price | money }} 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
54 |
55 | 56 |
57 |
58 | 59 | Total: 60 | {{ cart.total_price | money }} 61 | 62 |
63 | 64 | 65 |
66 |
67 | {%- else -%} 68 |

69 | The cart is empty. 70 | Continue shopping 71 |

72 | {%- endif -%} 73 |
74 |
75 |
76 |
77 | 78 | -------------------------------------------------------------------------------- /theme/snippets/address-form.liquid: -------------------------------------------------------------------------------- 1 | {% form 'customer_address', address, class: 'snippet-address-form' %} 2 | 78 | {% endform %} -------------------------------------------------------------------------------- /theme/snippets/main-menu.liquid: -------------------------------------------------------------------------------- 1 | 88 | -------------------------------------------------------------------------------- /src/templates/product.js: -------------------------------------------------------------------------------- 1 | import Swiper from 'swiper'; 2 | import uniq from 'lodash/uniq'; 3 | import { withElements } from 'with-elements'; 4 | import { ProductForm } from '@shopify/theme-product-form'; 5 | 6 | withElements('.template-product', async (templateElement) => { 7 | const sliderElement = templateElement.querySelector('.template-product__slider'); 8 | const prevBtn = sliderElement.querySelector('.carousel-control-prev'); 9 | const nextBtn = sliderElement.querySelector('.carousel-control-next'); 10 | 11 | const slider = new Swiper(sliderElement, { 12 | loop: true, 13 | slidesPerView: 1, 14 | spaceBetween: 0, 15 | }); 16 | 17 | prevBtn.addEventListener('click', () => { 18 | slider.slidePrev(); 19 | }); 20 | 21 | nextBtn.addEventListener('click', () => { 22 | slider.slideNext(); 23 | }); 24 | 25 | // Product Variant Selector 26 | (async () => { 27 | const selectedValues = []; 28 | 29 | const formElement = templateElement.querySelector('.template-product__form'); 30 | const productHandle = formElement.dataset.productHandle; 31 | const productData = await fetch(`/products/${productHandle}.js`).then((res) => res.json()); 32 | const buttonElement = formElement.querySelector('button[type=submit]'); 33 | 34 | function proceeding() { 35 | const selectElements = formElement.querySelectorAll('.template-product__form-option select'); 36 | 37 | // Select Variants by Steps 38 | selectElements.forEach((formSelectElement, index) => { 39 | const currentSelectValue = formSelectElement.value; 40 | const nextSelectElement = selectElements[index + 1]; 41 | 42 | if (nextSelectElement) { 43 | if (currentSelectValue !== selectedValues[index]) { 44 | nextSelectElement.value = ''; 45 | } 46 | 47 | if (currentSelectValue === '') { 48 | nextSelectElement.disabled = true; 49 | } else { 50 | nextSelectElement.disabled = false; 51 | } 52 | 53 | selectedValues[index] = currentSelectValue; 54 | } 55 | }); 56 | 57 | // Disable Inappropriate Options 58 | const selectOneElement = selectElements[0]; 59 | const selectTwoElement = selectElements[1]; 60 | const selectThreeElement = selectElements[2]; 61 | 62 | if (selectTwoElement) { 63 | const okayOptions = uniq( 64 | productData.variants.filter(({ option1 }) => option1 === selectOneElement.value).map(({ option2 }) => option2) 65 | ); 66 | 67 | const selectTwoOptionElements = selectTwoElement.querySelectorAll('option'); 68 | 69 | selectTwoOptionElements.forEach((selectTwoOptionElement) => { 70 | if (okayOptions.includes(selectTwoOptionElement.value)) { 71 | selectTwoOptionElement.disabled = false; 72 | } else { 73 | selectTwoOptionElement.disabled = true; 74 | } 75 | }); 76 | } 77 | 78 | if (selectThreeElement) { 79 | const okayOptions = uniq( 80 | productData.variants 81 | .filter(({ option1, option2 }) => option1 === selectOneElement.value && option2 === selectTwoElement.value) 82 | .map(({ option3 }) => option3) 83 | ); 84 | 85 | const selectThreeOptionElements = selectThreeElement.querySelectorAll('option'); 86 | 87 | selectThreeOptionElements.forEach((selectThreeOptionElement) => { 88 | if (okayOptions.includes(selectThreeOptionElement.value)) { 89 | selectThreeOptionElement.disabled = false; 90 | } else { 91 | selectThreeOptionElement.disabled = true; 92 | } 93 | }); 94 | } 95 | } 96 | 97 | proceeding(); 98 | 99 | if (productData.variants.length > 1) { 100 | buttonElement.disabled = true; 101 | } 102 | 103 | new ProductForm(formElement, productData, { 104 | onOptionChange: (event) => { 105 | const variant = event.dataset.variant; 106 | 107 | proceeding(); 108 | 109 | if (variant === null) { 110 | // The combination of selected options does not have a matching variant 111 | 112 | buttonElement.disabled = true; 113 | } else if (variant && !variant.available) { 114 | // The combination of selected options has a matching variant but it is 115 | // currently unavailable 116 | 117 | buttonElement.disabled = true; 118 | } else if (variant && variant.available) { 119 | // The combination of selected options has a matching variant and it is 120 | // available 121 | 122 | buttonElement.disabled = false; 123 | } 124 | 125 | // Slide to Current Variant Media 126 | sliderElement.querySelectorAll('.template-product__slider-slide').forEach((slideElement, index) => { 127 | if (+slideElement.dataset.mediaId === variant?.featured_media?.id) { 128 | slider.slideTo(index + 1); 129 | } 130 | }); 131 | }, 132 | }); 133 | })(); 134 | }); 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🛍 Bootstrap Shopify Theme 2 | 3 | A free [**Shopify Theme**](https://github.com/maxvien/bootstrap-shopify-theme) built with [**Bootstrap**](https://getbootstrap.com/), [BEM](http://getbem.com/), [Liquid](https://shopify.github.io/liquid/), [Sass](https://sass-lang.com/), [ESNext](https://en.wikipedia.org/wiki/ECMAScript#ES.Next), [Theme Tools](https://shopify.dev/tools/themes), ... and [Webpack](https://webpack.js.org/). 4 | 5 | ## Experience 6 | 7 | When I started building this Shopify theme, I thought it would be simple. But as I got deeper, I realized there was more to it. 8 | 9 | I build themes from scratch, using [Bootstrap](https://getbootstrap.com/) to create a clean, user-friendly interface. I follow the [BEM Methodology](http://getbem.com/), which helps keep my code minimal and reusable. 10 | 11 | I work with [Liquid](https://shopify.github.io/liquid/), [SASS](https://sass-lang.com/), [ESNext](https://en.wikipedia.org/wiki/ECMAScript#ES.Next) to keep the theme modern and flexible. When problems arise, [Shopify Theme Scripts](https://github.com/Shopify/theme-scripts) help me solve them faster. I also use [Shopify Metafield](https://shopify.dev/docs/admin-api/rest/reference/metafield) to add extra details where needed. 12 | 13 | For visuals, I rely on [Swiper](https://swiperjs.com/) to create smooth, touch-friendly sliders and [CSS Media Queries](https://www.w3schools.com/css/css_rwd_mediaqueries.asp) to ensure a responsive, mobile-first design. 14 | 15 | To streamline development, I use the [Shopify Theme CLI](https://shopify.dev/themes/tools/cli) for deployment and [Webpack Encore](https://github.com/symfony/webpack-encore) to bundle assets. [PostCSS](https://postcss.org/) and [CoreJS](https://github.com/zloirock/core-js) help keep the theme compatible with older browsers. 16 | 17 | And of course, I follow best practices with [Shopify Theme Check](https://shopify.dev/themes/tools/theme-check), [ESlint](https://eslint.org/), [Stylelint](https://stylelint.io/), [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)—because clean code makes everything easier. 18 | 19 | It’s a process. A learning curve. But in the end, it’s about crafting themes that work beautifully—for both the store owner and the customer. 20 | 21 | If you like this project, hit the **STAR** button to bookmark it ⭐️ 22 | 23 | ## Demonstration 24 | 25 | - **Store Link**: https://maxvien-bootstrap.myshopify.com 26 | - **Store Password**: `maxvien` 27 | 28 | ## Installation 29 | 30 | Clone the source code into your computer. 31 | 32 | ```bash 33 | git clone https://github.com/VienDinhCom/bootstrap-shopify-theme.git 34 | ``` 35 | 36 | **This project was developed with Node 18 and NPM 10.**
37 | 38 | To set up a compatible environment, please download [Volta](https://github.com/volta-cli/volta) and run `volta setup`. 39 | 40 | Then, install the project's dependencies. 41 | 42 | ```bash 43 | npm install 44 | ``` 45 | 46 | ## Usage 47 | 48 | First of all, you need to install [Shopify CLI](https://shopify.dev/apps/tools/cli/installation) and login into your online store. 49 | 50 | ```bash 51 | shopify login --store=your-store.myshopify.com 52 | ``` 53 | 54 | Then you can run the below commands to work with the theme. 55 | 56 | ### Serve 57 | 58 | Run `webpack watch` and `serve` the theme in development mode. 59 | 60 | ```bash 61 | npm run serve 62 | ``` 63 | 64 | ### Push 65 | 66 | Run `webpack build` and `push` the theme to your online store in production mode. 67 | 68 | ```bash 69 | npm run push 70 | ``` 71 | 72 | ### Pull 73 | 74 | Safely `add` the current project files to the git staging area, then `pull` the theme from your online store, and `clean` untracked asset files. 75 | 76 | ```bash 77 | npm run pull 78 | ``` 79 | 80 | ### Test 81 | 82 | Run unit `test` with jest and make sure the files are following the project workflow. 83 | 84 | ```bash 85 | npm run test 86 | ``` 87 | 88 | ### Lint 89 | 90 | Analyze the code to find problems with `shopify theme check`, `eslint`, `stylelint` and `prettier`. 91 | 92 | ```bash 93 | npm run lint 94 | ``` 95 | 96 | Automatically fix problems. 97 | 98 | ```bash 99 | npm run fix 100 | ``` 101 | 102 | ## Notes 103 | 104 | ### Theme Assets 105 | 106 | All files inside the `theme/assets` directory are ignored by `git`, except files starting with the `static` keyword in their filename. 107 | 108 | ### Webpack Encore 109 | 110 | [Symfony Webpack Encore](https://symfony.com/doc/current/frontend.html) is a simpler way to integrate Webpack into your application. It wraps Webpack, giving you a clean & powerful API for bundling JavaScript modules, pre-processing CSS & JS and compiling and minifying assets. Encore gives you professional asset system that’s a delight to use. 111 | 112 | If you want to use [React](https://symfony.com/doc/current/frontend/encore/reactjs.html) or [Vue](https://symfony.com/doc/current/frontend/encore/vuejs.html) in the theme, you can follow the documentation [here](https://symfony.com/doc/current/frontend.html). 113 | 114 | ## Visual Studio Code Extensions 115 | 116 | To speed up your productivity, you can install these extensions: 117 | 118 | - [Eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 119 | - [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 120 | - [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint) 121 | - [Shopify Liquid](https://marketplace.visualstudio.com/items?itemName=Shopify.theme-check-vscode) 122 | - [IntelliSense for CSS](https://marketplace.visualstudio.com/items?itemName=Zignd.html-css-class-completion) 123 | - [Material Icon Theme](https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme) 124 | - [Visual Studio IntelliCode](https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicode) 125 | 126 | ## Related Projects 127 | 128 | - **[Next Shopify Storefront](https://github.com/Maxvien/next-shopify-storefront)** • A Shopping Cart built with TypeScript, Tailwind CSS, Headless UI, Next.js, React.js, Shopify Hydrogen React,... and Shopify Storefront GraphQL API. 129 | - **[Shopify Data Faker](https://github.com/Maxvien/shopify-data-faker)** • A Shopify development tool for generating dummy store data. 130 | -------------------------------------------------------------------------------- /theme/templates/product.liquid: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {% for media in product.media %} 10 |
11 | {% case media.media_type %} 12 | {% when 'image' %} 13 | {% render 'image' image: media, width: 450, height: 450 %} 14 | {% when 'external_video' %} 15 | {{ media | external_video_tag }} 16 | {% when 'video' %} 17 | {{ media | video_tag: controls: true }} 18 | {% when 'model' %} 19 | {{ media | model_viewer_tag }} 20 | {% else %} 21 | {{ media | media_tag }} 22 | {% endcase %} 23 |
24 | {% endfor %} 25 |
26 | 27 | {% if product.media.size > 1 %} 28 | 32 | 36 | {% endif %} 37 |
38 |
39 | 40 |
41 | 42 | {% form 'product', product, class: 'template-product__form', data-product-handle: product.handle %} 43 | 44 |

{{ product.title }}

45 | 46 |
{{ product.description | strip_html }}
47 | 48 |

{{ product.price | money}}

49 | 50 | 64 | 65 | {% if product.has_only_default_variant %} 66 | 67 | {% else %} 68 | {% for option in product.options_with_values %} 69 |
70 | 73 | 84 |
85 | {% endfor %} 86 | {% endif %} 87 | 88 | 89 |
90 | 91 | 92 |
93 | 94 | 95 | 96 | {% endform %} 97 |
98 |
99 | 100 |
101 |
102 | 110 |
111 |
112 | {{ product.content }} 113 |
114 | 115 |
116 | {% if product.metafields.details != blank %} 117 | 118 | {% if product.metafields.details.material != blank %} 119 | 120 | 121 | 122 | 123 | {% endif %} 124 | {% if product.metafields.details.weight != blank %} 125 | 126 | 127 | 128 | 129 | {% endif %} 130 |
Material{{ product.metafields.details.material }}
Weight{{ product.metafields.details.weight }}
131 | {% endif %} 132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | -------------------------------------------------------------------------------- /theme/config/settings_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "current": { 3 | "sections": { 4 | "16214099630eec9cc3": { 5 | "type": "slideshow", 6 | "blocks": { 7 | "66040de5-ead7-4970-adb2-475b87d8343c": { 8 | "type": "item", 9 | "settings": { 10 | "title": "Bootstrap Shopify Theme", 11 | "description": "

A free Shopify Theme built with Bootstrap, BEM, Theme Tools, Swiper, Gulp, Parcel, Liquid, SASS, PostCSS, ESNext, ... and Passion.

", 12 | "actionButton": "Get Source Code", 13 | "image": "shopify://shop_images/working-at-night.jpg" 14 | } 15 | } 16 | }, 17 | "block_order": ["66040de5-ead7-4970-adb2-475b87d8343c"], 18 | "settings": {} 19 | }, 20 | "16214103457aee0d39": { 21 | "type": "featured-products", 22 | "blocks": { 23 | "dda11fea-fd7e-4b4f-97d6-ac3e2e6a7420": { 24 | "type": "item", 25 | "settings": { 26 | "product": "chain-bracelet" 27 | } 28 | }, 29 | "c529e3e0-8f76-442d-8159-7c6010d7decd": { 30 | "type": "item", 31 | "settings": { 32 | "product": "leather-anchor" 33 | } 34 | }, 35 | "5b5e0678-c4e8-4e25-bcd9-11489a870f81": { 36 | "type": "item", 37 | "settings": { 38 | "product": "antique-drawers" 39 | } 40 | }, 41 | "9ecb41ca-f2e7-4771-88b9-33d62267489f": { 42 | "type": "item", 43 | "settings": { 44 | "product": "bangle-bracelet" 45 | } 46 | }, 47 | "ffa9fa33-ec8b-491a-9a73-d957f0e1d2df": { 48 | "type": "item", 49 | "settings": { 50 | "product": "bedside-table" 51 | } 52 | }, 53 | "4ee78bfe-e4f3-4e0b-bf65-52040e999579": { 54 | "type": "item", 55 | "settings": { 56 | "product": "biodegradable-cardboard-pots" 57 | } 58 | }, 59 | "f354b6f7-ca7b-4c45-a75d-f1f8421efadb": { 60 | "type": "item", 61 | "settings": { 62 | "product": "black-bean-bag" 63 | } 64 | }, 65 | "4f1e57a5-c5b5-42df-a6c9-0412c28ae6d8": { 66 | "type": "item", 67 | "settings": { 68 | "product": "chequered-red-shirt" 69 | } 70 | } 71 | }, 72 | "block_order": [ 73 | "dda11fea-fd7e-4b4f-97d6-ac3e2e6a7420", 74 | "c529e3e0-8f76-442d-8159-7c6010d7decd", 75 | "5b5e0678-c4e8-4e25-bcd9-11489a870f81", 76 | "9ecb41ca-f2e7-4771-88b9-33d62267489f", 77 | "ffa9fa33-ec8b-491a-9a73-d957f0e1d2df", 78 | "4ee78bfe-e4f3-4e0b-bf65-52040e999579", 79 | "f354b6f7-ca7b-4c45-a75d-f1f8421efadb", 80 | "4f1e57a5-c5b5-42df-a6c9-0412c28ae6d8" 81 | ], 82 | "settings": { 83 | "title": "Featured Products" 84 | } 85 | }, 86 | "162141044380a7180d": { 87 | "type": "featured-collections", 88 | "blocks": { 89 | "f79c04f9-41c9-4495-a248-f666ac4dba43": { 90 | "type": "item", 91 | "settings": { 92 | "collection": "armchair" 93 | } 94 | }, 95 | "931e01aa-d73c-43f2-addc-03fc071a8e72": { 96 | "type": "item", 97 | "settings": { 98 | "collection": "bag" 99 | } 100 | }, 101 | "03dd3d87-3d56-49c1-9015-1af759648543": { 102 | "type": "item", 103 | "settings": { 104 | "collection": "blouse" 105 | } 106 | }, 107 | "214648e7-8f6f-4d5a-ad0d-443b682c5641": { 108 | "type": "item", 109 | "settings": { 110 | "collection": "bracelet" 111 | } 112 | }, 113 | "d3c5f54f-0d87-4e27-bcd9-d8e77aec82ef": { 114 | "type": "item", 115 | "settings": { 116 | "collection": "candle" 117 | } 118 | }, 119 | "d0e70a85-5b1f-4ec4-be2a-f31b573f08da": { 120 | "type": "item", 121 | "settings": { 122 | "collection": "choker" 123 | } 124 | }, 125 | "0890ce35-c39b-4d29-a309-b2982f75561e": { 126 | "type": "item", 127 | "settings": { 128 | "collection": "drawer" 129 | } 130 | }, 131 | "343972b0-b6da-4327-bc63-081204c46726": { 132 | "type": "item", 133 | "settings": { 134 | "collection": "earring" 135 | } 136 | }, 137 | "53dbfb8d-ab63-40cb-a9a4-923bcb620dac": { 138 | "type": "item", 139 | "settings": { 140 | "collection": "fence" 141 | } 142 | } 143 | }, 144 | "block_order": [ 145 | "f79c04f9-41c9-4495-a248-f666ac4dba43", 146 | "931e01aa-d73c-43f2-addc-03fc071a8e72", 147 | "03dd3d87-3d56-49c1-9015-1af759648543", 148 | "214648e7-8f6f-4d5a-ad0d-443b682c5641", 149 | "d3c5f54f-0d87-4e27-bcd9-d8e77aec82ef", 150 | "d0e70a85-5b1f-4ec4-be2a-f31b573f08da", 151 | "0890ce35-c39b-4d29-a309-b2982f75561e", 152 | "343972b0-b6da-4327-bc63-081204c46726", 153 | "53dbfb8d-ab63-40cb-a9a4-923bcb620dac" 154 | ], 155 | "settings": { 156 | "title": "Featured Collections" 157 | } 158 | }, 159 | "16214105149e606985": { 160 | "type": "featured-articles", 161 | "blocks": { 162 | "f2786828-4216-4342-ab24-382511c86db1": { 163 | "type": "item", 164 | "settings": { 165 | "article": "news/dolores-quis-aliquam-delectus-voluptatem-quae-et" 166 | } 167 | }, 168 | "2555878b-7b1f-4b61-ad3a-b28a85c7d642": { 169 | "type": "item", 170 | "settings": { 171 | "article": "news/qui-saepe-ratione-reprehenderit-labore-veritatis-animi-tempora-autem" 172 | } 173 | }, 174 | "8c332891-1e3e-4805-89ae-38f739cb3304": { 175 | "type": "item", 176 | "settings": { 177 | "article": "news/optio-dicta-ut-earum-veniam-eum-consequatur-laborum" 178 | } 179 | }, 180 | "98c08ec6-9ca9-43a2-984c-bf336b9fbe52": { 181 | "type": "item", 182 | "settings": { 183 | "article": "news/maiores-itaque-qui-et" 184 | } 185 | }, 186 | "cf9605ca-e217-43f3-93ee-3b21943f25c0": { 187 | "type": "item", 188 | "settings": { 189 | "article": "news/quidem-vero-suscipit-dolore-ad-maiores-deleniti-delectus" 190 | } 191 | }, 192 | "532f857d-4f6d-42d2-a06b-7989188b34d7": { 193 | "type": "item", 194 | "settings": { 195 | "article": "news/facilis-ipsam-at-nesciunt-enim" 196 | } 197 | }, 198 | "5e05500b-02d2-4d11-a30b-8aa28860e8bd": { 199 | "type": "item", 200 | "settings": { 201 | "article": "news/ut-voluptates-cumque-eligendi-ea-sapiente-rerum-quia" 202 | } 203 | }, 204 | "08435eb3-9e5c-4c84-8efd-b5164449b1c3": { 205 | "type": "item", 206 | "settings": { 207 | "article": "news/doloribus-quae-aliquam-accusamus-ea-optio-distinctio-quasi" 208 | } 209 | } 210 | }, 211 | "block_order": [ 212 | "f2786828-4216-4342-ab24-382511c86db1", 213 | "2555878b-7b1f-4b61-ad3a-b28a85c7d642", 214 | "8c332891-1e3e-4805-89ae-38f739cb3304", 215 | "98c08ec6-9ca9-43a2-984c-bf336b9fbe52", 216 | "cf9605ca-e217-43f3-93ee-3b21943f25c0", 217 | "532f857d-4f6d-42d2-a06b-7989188b34d7", 218 | "5e05500b-02d2-4d11-a30b-8aa28860e8bd", 219 | "08435eb3-9e5c-4c84-8efd-b5164449b1c3" 220 | ], 221 | "settings": { 222 | "title": "Featured Articles" 223 | } 224 | } 225 | }, 226 | "content_for_index": ["16214099630eec9cc3", "16214103457aee0d39", "162141044380a7180d", "16214105149e606985"] 227 | }, 228 | "presets": { 229 | "Default": {} 230 | } 231 | } 232 | --------------------------------------------------------------------------------