├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .huskyrc ├── .stylelintrc ├── Readme.md ├── TODO ├── build-utils ├── addons │ └── webpack.bundleanalyzer.js ├── common-path.js ├── common-plugins │ ├── copy-plugin.js │ └── replace-in-file-plugin.js ├── markup │ ├── className.js │ ├── html.js │ └── index.js ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js ├── commitlint.config.js ├── docs ├── assets │ ├── css │ │ └── main.css │ ├── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png │ └── js │ │ ├── main.js │ │ └── search.js ├── index.html ├── interfaces │ ├── helpers_lazyload.ilazyglobalconfig.html │ ├── types_classname_swiper_type.iswiperclassname.html │ ├── types_shopify_cart_type.icart.html │ ├── types_shopify_cart_type.iitemsresponse.html │ ├── types_shopify_cart_type.iproperty.html │ ├── types_shopify_common_type.ilineitem.html │ ├── types_shopify_common_type.ioption.html │ ├── types_shopify_common_type.ivariant.html │ ├── types_shopify_product_type.imedia.html │ ├── types_shopify_product_type.ipreviewobject.html │ ├── types_shopify_product_type.iproduct.html │ └── types_shopify_theme_type.itheme.html ├── modules.html └── modules │ ├── helpers.html │ ├── helpers_cart_cart.html │ ├── helpers_dom_common.html │ ├── helpers_dom_dom.html │ ├── helpers_dom_event.html │ ├── helpers_lazyload.html │ ├── helpers_sections.html │ ├── helpers_swiper.html │ ├── helpers_utils.html │ ├── helpers_vueconfig.html │ ├── types_classname_swiper_type.html │ ├── types_shopify_cart_type.html │ ├── types_shopify_common_type.html │ ├── types_shopify_product_type.html │ └── types_shopify_theme_type.html ├── jest.config.js ├── jest.setup.ts ├── package-lock.json ├── package.json ├── shopify ├── assets │ ├── base.css │ ├── cart-notification.js │ ├── cart.js │ ├── collage.css │ ├── collection-filters-form.js │ ├── component-accordion.css │ ├── component-article-card.css │ ├── component-badge.css │ ├── component-card.css │ ├── component-cart-items.css │ ├── component-cart-notification.css │ ├── component-cart.css │ ├── component-collection-hero.css │ ├── component-deferred-media.css │ ├── component-discounts.css │ ├── component-image-with-text.css │ ├── component-list-menu.css │ ├── component-list-payment.css │ ├── component-list-social.css │ ├── component-loading-overlay.css │ ├── component-menu-drawer.css │ ├── component-model-viewer-ui.css │ ├── component-newsletter.css │ ├── component-pagination.css │ ├── component-pickup-availability.css │ ├── component-price.css │ ├── component-product-model.css │ ├── component-rte.css │ ├── component-search.css │ ├── component-slider.css │ ├── component-totals.css │ ├── customer.css │ ├── customer.js │ ├── details-disclosure.js │ ├── details-modal.js │ ├── disclosure.css │ ├── global.js │ ├── newsletter-section.css │ ├── password-modal.js │ ├── pickup-availability.js │ ├── product-form.js │ ├── product-model.js │ ├── section-blog-post.css │ ├── section-collection-list.css │ ├── section-contact-form.css │ ├── section-featured-blog.css │ ├── section-footer.css │ ├── section-image-banner.css │ ├── section-main-blog.css │ ├── section-main-page.css │ ├── section-main-product.css │ ├── section-multicolumn.css │ ├── section-password.css │ ├── section-product-recommendations.css │ ├── section-rich-text.css │ ├── share.js │ ├── slider.js │ ├── template-collection.css │ ├── template-giftcard.css │ └── variants.js ├── config │ ├── settings_data.json │ └── settings_schema.json ├── layout │ ├── password.liquid │ └── theme.liquid ├── locales │ ├── bg-BG.json │ ├── cs.json │ ├── cs.schema.json │ ├── da.json │ ├── da.schema.json │ ├── de.json │ ├── de.schema.json │ ├── el.json │ ├── en.default.json │ ├── en.default.schema.json │ ├── es.json │ ├── es.schema.json │ ├── fi.json │ ├── fi.schema.json │ ├── fr.json │ ├── fr.schema.json │ ├── hr-HR.json │ ├── hu.json │ ├── id.json │ ├── it.json │ ├── it.schema.json │ ├── ja.json │ ├── ja.schema.json │ ├── ko.json │ ├── ko.schema.json │ ├── lt-LT.json │ ├── nb.json │ ├── nb.schema.json │ ├── nl.json │ ├── nl.schema.json │ ├── pl.json │ ├── pl.schema.json │ ├── pt-BR.json │ ├── pt-BR.schema.json │ ├── pt-PT.json │ ├── pt-PT.schema.json │ ├── ro-RO.json │ ├── ru.json │ ├── sk-SK.json │ ├── sl-SI.json │ ├── sv.json │ ├── sv.schema.json │ ├── th.json │ ├── th.schema.json │ ├── tr.json │ ├── tr.schema.json │ ├── vi.json │ ├── vi.schema.json │ ├── zh-CN.json │ ├── zh-CN.schema.json │ ├── zh-TW.json │ └── zh-TW.schema.json ├── sections │ ├── announcement-bar.liquid │ ├── apps.liquid │ ├── cart-icon-bubble.liquid │ ├── cart-live-region-text.liquid │ ├── cart-notification-button.liquid │ ├── cart-notification-product.liquid │ ├── collage.liquid │ ├── collection-list.liquid │ ├── contact-form.liquid │ ├── custom-liquid.liquid │ ├── featured-blog.liquid │ ├── featured-collection.liquid │ ├── footer.liquid │ ├── header.liquid │ ├── image-banner.liquid │ ├── image-with-text.liquid │ ├── main-404.liquid │ ├── main-article.liquid │ ├── main-blog.liquid │ ├── main-cart-footer.liquid │ ├── main-cart-items.liquid │ ├── main-collection-banner.liquid │ ├── main-collection-product-grid.liquid │ ├── main-list-collections.liquid │ ├── main-page.liquid │ ├── main-password-footer.liquid │ ├── main-password-header.liquid │ ├── main-product.liquid │ ├── main-search.liquid │ ├── multicolumn.liquid │ ├── newsletter.liquid │ ├── page.liquid │ ├── pickup-availability.liquid │ ├── product-recommendations.liquid │ ├── rich-text.liquid │ └── test │ │ ├── Test.ts │ │ ├── _test.scss │ │ └── test.liquid ├── snippets │ ├── article-card.liquid │ ├── cart-notification.liquid │ ├── icon-3d-model.liquid │ ├── icon-accordion.liquid │ ├── icon-account.liquid │ ├── icon-arrow.liquid │ ├── icon-caret.liquid │ ├── icon-cart-empty.liquid │ ├── icon-cart.liquid │ ├── icon-checkmark.liquid │ ├── icon-clipboard.liquid │ ├── icon-close-small.liquid │ ├── icon-close.liquid │ ├── icon-discount.liquid │ ├── icon-error.liquid │ ├── icon-facebook.liquid │ ├── icon-filter.liquid │ ├── icon-hamburger.liquid │ ├── icon-instagram.liquid │ ├── icon-minus.liquid │ ├── icon-padlock.liquid │ ├── icon-pinterest.liquid │ ├── icon-play.liquid │ ├── icon-plus.liquid │ ├── icon-remove.liquid │ ├── icon-share.liquid │ ├── icon-snapchat.liquid │ ├── icon-success.liquid │ ├── icon-tick.liquid │ ├── icon-tiktok.liquid │ ├── icon-tumblr.liquid │ ├── icon-twitter.liquid │ ├── icon-unavailable.liquid │ ├── icon-vimeo.liquid │ ├── icon-youtube.liquid │ ├── icon-zoom.liquid │ ├── meta-tags.liquid │ ├── pagination.liquid │ ├── price.liquid │ ├── product-card-placeholder.liquid │ ├── product-card.liquid │ ├── product-thumbnail.liquid │ └── social-sharing.liquid └── templates │ ├── 404.json │ ├── article.json │ ├── blog.json │ ├── cart.json │ ├── collection.json │ ├── customers │ ├── account.liquid │ ├── activate_account.liquid │ ├── addresses.liquid │ ├── login.liquid │ ├── order.liquid │ ├── register.liquid │ └── reset_password.liquid │ ├── gift_card.liquid │ ├── index.json │ ├── list-collections.json │ ├── page.contact.json │ ├── page.json │ ├── password.json │ ├── product.json │ └── search.json ├── src ├── __test__ │ ├── cart │ │ └── cart.spec.ts │ ├── dom │ │ └── dom.spec.ts │ └── test │ │ └── app.spec.ts ├── helpers │ ├── cart │ │ └── cart.ts │ ├── dom │ │ ├── common.ts │ │ ├── dom.ts │ │ └── event.ts │ ├── index.ts │ └── sections │ │ └── index.ts ├── index.ts ├── styles │ ├── _general.scss │ ├── _vueGeneral.scss │ ├── base │ │ ├── _base-color.scss │ │ ├── _base-dir.scss │ │ └── _heading-base.scss │ ├── components │ │ ├── _components-dir.scss │ │ ├── button │ │ │ └── _btn.scss │ │ ├── loading │ │ │ └── _loading-ui.scss │ │ └── toast │ │ │ └── _toast.scss │ ├── helpers │ │ └── _helpers-dir.scss │ ├── layout │ │ └── _layout-dir.scss │ ├── main.scss │ ├── pages │ │ ├── _body.scss │ │ ├── _pages-dir.scss │ │ ├── collection │ │ │ ├── _all-collection.scss │ │ │ ├── _normal-collection.scss │ │ │ └── _page-alles.scss │ │ └── customer │ │ │ └── _account.scss │ ├── sections │ │ └── _sections-dir.scss │ ├── snippet │ │ └── _background-image.scss │ ├── utils │ │ ├── _utils-dir.scss │ │ ├── functions │ │ │ └── _get-variable-css.scss │ │ └── mixins │ │ │ ├── _center.scss │ │ │ ├── _position.scss │ │ │ ├── _prefix.scss │ │ │ ├── _responsive.scss │ │ │ ├── _size.scss │ │ │ └── _three-dots.scss │ └── vendors │ │ ├── _grid.scss │ │ ├── _normalize.scss │ │ ├── _reset.scss │ │ └── _variants.scss ├── types │ ├── shopify │ │ ├── cart.type.ts │ │ ├── collection.type.ts │ │ ├── common.type.ts │ │ ├── product.type.ts │ │ └── theme.type.ts │ └── vue-shims.d.ts └── vue │ ├── components │ ├── entry │ │ ├── cart │ │ │ └── .gitkeep │ │ ├── index.ts │ │ ├── product │ │ │ └── .gitkeep │ │ ├── readme.md │ │ └── search │ │ │ └── .gitkeep │ └── globals │ │ ├── .gitkeep │ │ ├── XoButton.vue │ │ └── readme.md │ ├── config │ └── index.ts │ ├── filters │ ├── hugMoneyFormat.ts │ ├── hugUppercase.ts │ ├── imgURL.ts │ └── index.ts │ ├── mixins │ ├── CollectionCard.ts │ └── SplitingVariant.ts │ └── store │ ├── index.ts │ ├── modules │ ├── cart │ │ └── index.ts │ └── collection │ │ └── index.ts │ └── type.ts ├── tsconfig.json ├── typedoc.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | ["@babel/proposal-decorators", { "legacy": true }], 7 | ["@babel/proposal-class-properties", { "loose": true }] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | plugins: [ 4 | "@typescript-eslint", 5 | // "eslint-comments", 6 | // "promise", 7 | // "unicorn", 8 | ], 9 | extends: [ 10 | "airbnb-typescript/base", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:eslint-comments/recommended", 13 | 'plugin:vue/vue3-recommended' 14 | ], 15 | parserOptions: { 16 | project: './tsconfig.json', 17 | }, 18 | overrides: [ 19 | { 20 | // enable the rule specifically for TypeScript files 21 | "files": ["*.ts", "*.vue"], 22 | "rules": { 23 | "@typescript-eslint/explicit-function-return-type": ["error"] 24 | } 25 | } 26 | ], 27 | rules: { 28 | "no-underscore-dangle": 'off', 29 | 'max-len': 'off', 30 | 'import/no-cycle': 'off', 31 | 32 | /** 33 | * Lỗi ngoại trừ : 34 | * a || b 35 | * a && b() 36 | * a() || (b = c) 37 | * a ? b() : c() 38 | * a ? b() || (c = d) : e() 39 | */ 40 | "no-unused-expressions": "off", 41 | "@typescript-eslint/no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }], 42 | 43 | /** 44 | * Cho phép ngắt dòng ( string dom ) 45 | */ 46 | "operator-linebreak": "off", 47 | 48 | /** 49 | * For mutations VueX 50 | * 51 | * setCart(state, payload) { 52 | * state.errorMessage = false; 53 | * state.shoppingCart = payload; 54 | * return state; 55 | }, 56 | */ 57 | "no-param-reassign": ["error", { "props": true, "ignorePropertyModificationsFor": ["state"] }], 58 | "no-shadow": "off", 59 | /** 60 | * Ignore this vue lifecycle 61 | */ 62 | "class-methods-use-this": [ 63 | "error", 64 | { "exceptMethods": [ 65 | "beforeCreate", 66 | "created", 67 | "beforeMount", 68 | "mounted", 69 | "beforeUpdate", 70 | "updated", 71 | "beforeDestroy", 72 | "destroyed" 73 | ] 74 | } 75 | ], 76 | "import/no-extraneous-dependencies": ["error", {"devDependencies": true}] 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | thumb.db 3 | *.zip 4 | *.rar 5 | .DS_Store 6 | npm-debug.log 7 | debug.log 8 | .env 9 | dist/ 10 | dist/config.yml 11 | dist/assets/app.js 12 | dist/assets/main.css 13 | .vscode 14 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "husky": { 3 | "hooks": { 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignoreFiles": [ 3 | "src/styles/vendors/*.scss", 4 | ], 5 | "rules": { 6 | "max-nesting-depth": 2, 7 | "max-empty-lines": 2, 8 | "color-hex-case": "lower", 9 | "comment-empty-line-before": ["always", { 10 | "except": ["first-nested"], 11 | "ignore": ["after-comment", "stylelint-commands"] 12 | }] 13 | } 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - Tạo Global component không cần import + chạy được ở design mode 2 | -------------------------------------------------------------------------------- /build-utils/addons/webpack.bundleanalyzer.js: -------------------------------------------------------------------------------- 1 | const WebpackBundleAnalyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 2 | 3 | module.exports = { 4 | plugins: [ 5 | new WebpackBundleAnalyzer(), 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /build-utils/common-path.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | /** 5 | * Input path 6 | */ 7 | srcPath: path.resolve(__dirname, '../src'), 8 | 9 | /** 10 | * Folder path 11 | */ 12 | componentsPath: path.resolve(__dirname, '../src/components'), 13 | helpersPath: path.resolve(__dirname, '../src/helpers'), 14 | stylesPath: path.resolve(__dirname, '../src/styles'), 15 | typesPath: path.resolve(__dirname, '../src/types'), 16 | vuePath: path.resolve(__dirname, '../src/vue'), 17 | themeDevPath: path.resolve(__dirname, '../shopify'), // Shopify structure 18 | 19 | /** 20 | * Output path 21 | */ 22 | outputPath: path.resolve(__dirname, '../dist'), 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /build-utils/common-plugins/copy-plugin.js: -------------------------------------------------------------------------------- 1 | const CopyPlugin = require("copy-webpack-plugin"); 2 | const path = require('path'); 3 | const commonPath = require('../common-path'); 4 | 5 | module.exports = { 6 | huwngCopyPlugin: new CopyPlugin({ 7 | patterns: [ 8 | /** 9 | * Các folder có json, không lấy folder con 10 | */ 11 | { 12 | from: path.resolve(__dirname, commonPath.themeDevPath, 'config/*.json'), 13 | to: path.resolve(__dirname, commonPath.outputPath, 'config/[name].[ext]'), 14 | }, 15 | { 16 | from: path.resolve(__dirname, commonPath.themeDevPath, 'locales/*.json'), 17 | to: path.resolve(__dirname, commonPath.outputPath, 'locales/[name].[ext]'), 18 | }, 19 | /** 20 | * Các folder có liquid, có lấy folder con 21 | */ 22 | { 23 | 24 | from: path.resolve(__dirname, commonPath.themeDevPath, 'layout/**/*.liquid'), 25 | to: path.resolve(__dirname, commonPath.outputPath, 'layout/[name].[ext]'), 26 | }, 27 | { 28 | from: path.resolve(__dirname, commonPath.themeDevPath, 'sections/**/*.liquid'), 29 | to: path.resolve(__dirname, commonPath.outputPath, 'sections/[name].[ext]'), 30 | }, 31 | { 32 | from: path.resolve(__dirname, commonPath.themeDevPath, 'snippets/**/*.liquid'), 33 | to: path.resolve(__dirname, commonPath.outputPath, 'snippets/[name].[ext]'), 34 | }, 35 | /** 36 | * Folder này cứ để nguyên xi 37 | */ 38 | { 39 | from: path.resolve(__dirname, commonPath.themeDevPath, 'templates'), 40 | to: path.resolve(__dirname, commonPath.outputPath, 'templates'), 41 | }, 42 | { 43 | from: path.resolve(__dirname, commonPath.themeDevPath, 'assets'), 44 | to: path.resolve(__dirname, commonPath.outputPath, 'assets'), 45 | } 46 | // , 47 | // /** 48 | // * SCSS in section ( inside ./theme folder ) 49 | // */ 50 | // { 51 | // from: path.resolve(__dirname, commonPath.themeDevPath, '**/*.scss'), 52 | // to: path.resolve(__dirname, commonPath.themeDevPath, 'sections/scss/[name].[ext]'), 53 | // }, 54 | // /** 55 | // * TS in section ( inside ./theme folder ) 56 | // */ 57 | // { 58 | // from: path.resolve(__dirname, commonPath.themeDevPath, '**/*.ts'), 59 | // to: path.resolve(__dirname, commonPath.themeDevPath, 'sections/ts/[name].[ext]'), 60 | // } 61 | ], 62 | }), 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /build-utils/common-plugins/replace-in-file-plugin.js: -------------------------------------------------------------------------------- 1 | const ReplaceInFileWebpackPlugin = require('replace-in-file-webpack-plugin'); 2 | const path = require('path'); 3 | const commonPath = require('../common-path'); 4 | const all = require('../markup/index'); 5 | 6 | module.exports = { 7 | huwngReplacePlugin: new ReplaceInFileWebpackPlugin(all.allReplace), 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /build-utils/markup/className.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Classname common markup 3 | * 4 | */ 5 | module.exports = { 6 | className: [{ 7 | dir: 'dist', 8 | test: [/\.css$/, /\.liquid/], 9 | rules: [{ 10 | search: '@IMG_HOVER_ZOOM', 11 | replace: 'xo-img--is-zoom' 12 | }, 13 | { 14 | search: '@EFFECT_ROTATE', 15 | replace: 'xo-effect--is-rotate' 16 | }, 17 | { 18 | search: '@EFFECT_MOVE_TO', 19 | replace: 'xo-effect--moveto' 20 | }, 21 | ] 22 | }], 23 | } 24 | -------------------------------------------------------------------------------- /build-utils/markup/html.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTML ( base on Liquid ) common markup 3 | * 4 | */ 5 | // test: [/\.css$/, /\.liquid/], 6 | 7 | const FULL_HEADING = { 8 | liquid: ` 9 | {% case section.settings.align_header %} 10 | {% when 'left' %} 11 | {% assign align_class = 'text-left' %} 12 | {% when 'center' %} 13 | {% assign align_class = 'text-center' %} 14 | {% when 'right' %} 15 | {% assign align_class = 'text-right' %} 16 | {% else %} 17 | 18 | {% endcase %} 19 | 20 |
21 |

{{ section.settings.heading}}

22 | {% if section.settings.subheading == blank %} 23 |
24 | {% else %} 25 |
26 |

{{ section.settings.subheading }}

27 |
28 |
29 | {% endif %} 30 |
31 | `, 32 | 33 | jsonSchemaSetting: ` 34 | { 35 | "type" : "header", 36 | "content": "Section header" 37 | }, 38 | { 39 | "type": "text", 40 | "id": "heading", 41 | "label": "Heading", 42 | "default": "Heading of section" 43 | }, 44 | { 45 | "type": "textarea", 46 | "id": "subheading", 47 | "label": "Sub heading", 48 | "default": "Sub heading of section" 49 | }, 50 | { 51 | "type": "select", 52 | "id": "align_header", 53 | "label": "Align Header", 54 | "options": [ 55 | { "value": "left", "label": "Left" }, 56 | { "value": "center", "label": "Center" }, 57 | { "value": "right", "label": "Right" } 58 | ], 59 | "default": "center" 60 | } 61 | ` 62 | }; 63 | 64 | module.exports = { 65 | html: [{ 66 | dir: 'dist', 67 | test: [/\.liquid/], 68 | rules: [{ 69 | search: '@HTML_FULL_HEADING', 70 | replace: FULL_HEADING.liquid 71 | }, { 72 | search: '@SETTING_FULL_HEADING', 73 | replace: FULL_HEADING.jsonSchemaSetting 74 | }] 75 | }] 76 | } 77 | -------------------------------------------------------------------------------- /build-utils/markup/index.js: -------------------------------------------------------------------------------- 1 | const htmlReplace = require('./html'); 2 | const classReplace = require('./className'); 3 | 4 | let boutiqueReplace = [ 5 | ...classReplace.className, 6 | ...htmlReplace.html, 7 | ]; 8 | 9 | module.exports = { 10 | allReplace: [...boutiqueReplace] 11 | } 12 | -------------------------------------------------------------------------------- /build-utils/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const commonPath = require('./common-path'); 3 | const TerserJSPlugin = require('terser-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 6 | const { VueLoaderPlugin } = require('vue-loader') 7 | const StyleLintPlugin = require('stylelint-webpack-plugin'); 8 | const copyPlugin = require('./common-plugins/copy-plugin'); 9 | const replacePlugin = require('./common-plugins/replace-in-file-plugin'); 10 | 11 | const hugCommonConfig = { 12 | name: 'ShopiyThemeStarter', 13 | entry: './src/index.ts', 14 | output: { 15 | path: commonPath.outputPath, 16 | filename: 'assets/app.js', 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | exclude: /node_modules/, 23 | use: [ 24 | { 25 | loader: 'babel-loader', 26 | }, 27 | 'webpack-import-glob-loader' /** @see https://www.npmjs.com/package/import-glob-loader */ 28 | ] 29 | }, 30 | { 31 | test: /\.ts?$/, 32 | loader: 'ts-loader', 33 | exclude: /node_modules/, 34 | options: { 35 | appendTsSuffixTo: [/\.vue$/], 36 | }, 37 | }, 38 | { 39 | test: /\.vue$/, 40 | loader: 'vue-loader', 41 | options: { 42 | loaders: { 43 | 'scss': 'vue-style-loader!css-loader!sass-loader', 44 | 'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax', 45 | }, 46 | }, 47 | }, 48 | { 49 | test: /\.(s*)css$/, 50 | use: [ 51 | MiniCssExtractPlugin.loader, 52 | { 53 | loader: 'css-loader', 54 | options: { 55 | importLoaders: 2, 56 | sourceMap: false, 57 | }, 58 | }, 59 | { 60 | loader: 'postcss-loader', 61 | options: { 62 | plugins: () => [require('autoprefixer')], 63 | sourceMap: false, 64 | }, 65 | }, 66 | { 67 | loader: 'sass-loader', 68 | options: { 69 | sourceMap: false, 70 | }, 71 | }, 72 | 'webpack-import-glob-loader' /** @see https://www.npmjs.com/package/import-glob-loader */ 73 | ], 74 | }, 75 | // { 76 | // test: /\.(jpe?g|png|gif|woff|woff2|eot|ttf|svg)(\?[a-z0-9=.]+)?$/, 77 | // loader: 'url-loader?limit=100000' 78 | // } 79 | ], 80 | }, 81 | optimization: { 82 | minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})], 83 | }, 84 | plugins: [ 85 | new MiniCssExtractPlugin({ 86 | filename: 'assets/main.css', 87 | }), 88 | new VueLoaderPlugin(), 89 | new StyleLintPlugin({ 90 | configFile: '.stylelintrc', 91 | context: 'src', 92 | files: '**/*.(s(c|a)ss|css)', 93 | failOnError: false, 94 | quiet: false, 95 | emitErrors: true 96 | }), 97 | copyPlugin.huwngCopyPlugin, 98 | replacePlugin.huwngReplacePlugin, 99 | ], 100 | resolve: { 101 | extensions: ['.vue', '.ts', '.js', '.json'], 102 | alias: { 103 | vue: 'vue/dist/vue.esm-bundler.js', 104 | Components: commonPath.componentsPath, 105 | Helpers: commonPath.helpersPath, 106 | Styles: commonPath.stylesPath, 107 | Shopify: commonPath.themeDevPath, 108 | Types: commonPath.typesPath, 109 | Vue: commonPath.vuePath, 110 | } 111 | }, 112 | stats: { 113 | entrypoints: false, 114 | children: false, 115 | } 116 | }; 117 | 118 | module.exports = { hugCommonConfig } 119 | -------------------------------------------------------------------------------- /build-utils/webpack.dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'development', 3 | devtool: 'inline-source-map', 4 | devServer: { 5 | contentBase: './dist', 6 | hot: false, 7 | watchContentBase: true, 8 | }, 9 | watch: true, 10 | /** 11 | * Tells stats whether to add information about the built modules. 12 | * @see {@link https://webpack.js.org/configuration/stats/} 13 | */ 14 | stats: { 15 | excludeAssets: [ 16 | /.liquid/, 17 | /.json/, 18 | /.svg/, 19 | /.min.*/, 20 | /.png/, 21 | /.gif/ 22 | ], 23 | modules: false 24 | }, 25 | }; 26 | 27 | -------------------------------------------------------------------------------- /build-utils/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 2 | 3 | module.exports = { 4 | mode: 'production', 5 | optimization: { 6 | minimizer: [ 7 | new CssMinimizerPlugin(), 8 | ], 9 | }, 10 | }; 11 | 12 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | 6 | "setupFilesAfterEnv": ['/jest.setup.ts'], 7 | 8 | /** 9 | * Xác định nơi bỏ các file testing 10 | * Thông thuòng ra sẽ bỏ các file typescript vào hết thư mục src 11 | */ 12 | "testMatch": [ 13 | "**/__tests__/**/*.+(ts|tsx|js)", 14 | "**/?(*.)+(spec|test).+(ts|tsx|js)" 15 | ], 16 | 17 | /** 18 | * Jest sẽ dựa định dạng này để phát hiện các file cần được testing 19 | */ 20 | "transform": { 21 | "^.+\\.(ts|tsx|vue)$": "ts-jest" 22 | }, 23 | 24 | /** 25 | * Thằng ts-jest sẽ xác định các file có dạng này 26 | * Sau đó sẽ biến đổi về dạng nó có thể hiểu được để chạy jest 27 | */ 28 | "verbose": true, 29 | 30 | /** 31 | * Báo cáo các bài test lúc đang chạy 32 | */ 33 | "globals": { 34 | "ts-jest": { 35 | diagnostics: false 36 | } 37 | } 38 | /** 39 | * Cái này để các hàm của thằng jest trở thành globals 40 | * không cần phải require hay import khi dùng nữa 41 | */ 42 | } 43 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xo-boutique", 3 | "version": "1.0.0", 4 | "description": "HungDz", 5 | "main": "webpack.common.js", 6 | "scripts": { 7 | "dev": "npm run build -- --env.env=dev", 8 | "build:prod": "npm run build -- --env.env=prod", 9 | "build": "webpack", 10 | "build:dev:bundleanalyzer": "npm run build -- --env.env=dev --env.addons=bundleanalyzer", 11 | "build:prod:bundleanalyzer": "npm run build -- --env.env=prod --env.addons=bundleanalyzer", 12 | "docs": "npx typedoc", 13 | "test": "jest --detectOpenHandles", 14 | "lint:js": "eslint --ext \".ts,.vue\" --ignore-path .gitignore .", 15 | "lint:style": "stylelint \"**/*.{vue,css}\" --ignore-path .gitignore", 16 | "lint": "npm run lint:js && npm run lint:style" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git@gitlab.com:xopify-themes/xo-boutique.git" 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 25 | } 26 | }, 27 | "_moduleAliases": { 28 | "@components": "dist", 29 | "@helpers": "dist" 30 | }, 31 | "keywords": [], 32 | "author": "", 33 | "license": "ISC", 34 | "bugs": { 35 | "url": "https://gitlab.com/xopify-themes/xo-boutique/-/issues" 36 | }, 37 | "homepage": "https://gitlab.com/xopify-themes/xo-boutique", 38 | "devDependencies": { 39 | "@babel/core": "^7.5.5", 40 | "@babel/plugin-proposal-class-properties": "^7.12.1", 41 | "@babel/plugin-proposal-decorators": "^7.12.12", 42 | "@babel/preset-env": "^7.5.5", 43 | "@commitlint/cli": "^11.0.0", 44 | "@commitlint/config-conventional": "^11.0.0", 45 | "@shopify/theme-currency": "^4.1.1", 46 | "@shopify/theme-predictive-search": "^4.1.1", 47 | "@testing-library/jest-dom": "^5.11.9", 48 | "@types/google.maps": "^3.44.2", 49 | "@types/googlemaps": "^3.43.3", 50 | "@types/jest": "^26.0.20", 51 | "@types/swiper": "^5.4.2", 52 | "@typescript-eslint/eslint-plugin": "^4.14.1", 53 | "@typescript-eslint/parser": "^4.14.1", 54 | "@vue/compiler-sfc": "^3.1.2", 55 | "autoprefixer": "^9.7.1", 56 | "babel-loader": "^8.0.6", 57 | "babel-polyfill": "^6.26.0", 58 | "copy-webpack-plugin": "^6.2.1", 59 | "css-loader": "^3.2.0", 60 | "css-minimizer-webpack-plugin": "^1.2.0", 61 | "eslint": "^6.8.0", 62 | "eslint-config-airbnb": "^18.0.1", 63 | "eslint-config-airbnb-typescript": "^12.0.0", 64 | "eslint-import-resolver-alias": "^1.1.2", 65 | "eslint-plugin-eslint-comments": "^3.2.0", 66 | "eslint-plugin-import": "^2.22.1", 67 | "eslint-plugin-vue": "^7.12.1", 68 | "husky": "^4.3.8", 69 | "jest": "^26.6.3", 70 | "jsdoc": "^3.6.6", 71 | "mini-css-extract-plugin": "^0.9.0", 72 | "module-alias": "^2.2.2", 73 | "node-sass": "^4.12.0", 74 | "optimize-css-assets-webpack-plugin": "^5.0.3", 75 | "postcss-loader": "^3.0.0", 76 | "replace-in-file-webpack-plugin": "^1.0.6", 77 | "sass-loader": "^7.1.0", 78 | "style-loader": "^1.0.0", 79 | "stylelint": "^13.9.0", 80 | "stylelint-config-sass-guidelines": "^7.1.0", 81 | "stylelint-config-standard": "^20.0.0", 82 | "stylelint-webpack-plugin": "^2.1.1", 83 | "terser-webpack-plugin": "^2.2.1", 84 | "ts-jest": "^26.5.0", 85 | "ts-loader": "^7.0.5", 86 | "typedoc": "^0.20.32", 87 | "typescript": "^3.9.7", 88 | "url-loader": "^2.1.0", 89 | "vue-debounce-decorator": "^1.0.1", 90 | "vue-loader": "^16.3.0", 91 | "vue-style-loader": "^4.1.2", 92 | "webpack": "^4.38.0", 93 | "webpack-bundle-analyzer": "^4.4.0", 94 | "webpack-cli": "^3.3.6", 95 | "webpack-import-glob-loader": "^1.6.3", 96 | "webpack-merge": "^4.2.2" 97 | }, 98 | "dependencies": { 99 | "swiper": "^6.5.1", 100 | "vue": "^3.1.2", 101 | "vue-class-component": "^7.2.6", 102 | "vue-property-decorator": "^9.1.2", 103 | "vuex": "^3.6.2", 104 | "vuex-module-decorators": "^1.0.1" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /shopify/assets/cart-notification.js: -------------------------------------------------------------------------------- 1 | class CartNotification extends HTMLElement { 2 | constructor() { 3 | super(); 4 | 5 | this.notification = document.getElementById('cart-notification'); 6 | this.header = document.querySelector('sticky-header'); 7 | this.onBodyClick = this.handleBodyClick.bind(this); 8 | 9 | this.notification.addEventListener('keyup', (evt) => evt.code === 'Escape' && this.close()); 10 | this.querySelectorAll('button[type="button"]').forEach((closeButton) => 11 | closeButton.addEventListener('click', this.close.bind(this)) 12 | ); 13 | } 14 | 15 | open() { 16 | this.notification.classList.add('animate', 'active'); 17 | 18 | this.notification.addEventListener('transitionend', () => { 19 | this.notification.focus(); 20 | trapFocus(this.notification); 21 | }, { once: true }); 22 | 23 | document.body.addEventListener('click', this.onBodyClick); 24 | } 25 | 26 | close() { 27 | this.notification.classList.remove('active'); 28 | 29 | document.body.removeEventListener('click', this.onBodyClick); 30 | 31 | removeTrapFocus(this.activeElement); 32 | } 33 | 34 | renderContents(parsedState) { 35 | this.productId = parsedState.id; 36 | this.getSectionsToRender().forEach((section => { 37 | document.getElementById(section.id).innerHTML = 38 | this.getSectionInnerHTML(parsedState.sections[section.id], section.selector); 39 | })); 40 | 41 | this.header?.reveal(); 42 | this.open(); 43 | } 44 | 45 | getSectionsToRender() { 46 | return [ 47 | { 48 | id: 'cart-notification-product', 49 | selector: `#cart-notification-product-${this.productId}`, 50 | }, 51 | { 52 | id: 'cart-notification-button' 53 | }, 54 | { 55 | id: 'cart-icon-bubble' 56 | } 57 | ]; 58 | } 59 | 60 | getSectionInnerHTML(html, selector = '.shopify-section') { 61 | return new DOMParser() 62 | .parseFromString(html, 'text/html') 63 | .querySelector(selector).innerHTML; 64 | } 65 | 66 | handleBodyClick(evt) { 67 | const target = evt.target; 68 | if (target !== this.notification && !target.closest('cart-notification')) { 69 | const disclosure = target.closest('details-disclosure'); 70 | this.activeElement = disclosure ? disclosure.querySelector('summary') : null; 71 | this.close(); 72 | } 73 | } 74 | 75 | setActiveElement(element) { 76 | this.activeElement = element; 77 | } 78 | } 79 | 80 | customElements.define('cart-notification', CartNotification); 81 | -------------------------------------------------------------------------------- /shopify/assets/component-accordion.css: -------------------------------------------------------------------------------- 1 | .accordion summary { 2 | display: flex; 3 | position: relative; 4 | line-height: 1; 5 | padding: 1.5rem 0; 6 | } 7 | 8 | .accordion .summary__title { 9 | display: flex; 10 | flex: 1; 11 | } 12 | 13 | .accordion + .accordion { 14 | margin-top: 0; 15 | border-top: none; 16 | } 17 | 18 | .accordion { 19 | margin-top: 2.5rem; 20 | margin-bottom: 0; 21 | border-top: 0.1rem solid var(--color-foreground-20); 22 | border-bottom: 0.1rem solid var(--color-foreground-20); 23 | } 24 | 25 | .accordion__title { 26 | display: inline-block; 27 | max-width: calc(100% - 6rem); 28 | min-height: 1.6rem; 29 | margin: 0; 30 | word-break: break-word; 31 | } 32 | 33 | .accordion .icon-accordion { 34 | align-self: center; 35 | min-width: 1.6rem; 36 | margin-right: 1rem; 37 | fill: var(--color-foreground); 38 | } 39 | 40 | .accordion details[open] > summary .icon-caret { 41 | transform: rotate(180deg); 42 | } 43 | 44 | .accordion__content { 45 | margin-bottom: 1.5rem; 46 | word-break: break-word; 47 | } 48 | 49 | .accordion__content img { 50 | max-width: 100%; 51 | } 52 | -------------------------------------------------------------------------------- /shopify/assets/component-article-card.css: -------------------------------------------------------------------------------- 1 | .articles-wrapper.grid { 2 | margin: 0 0 5rem 0; 3 | } 4 | 5 | @media screen and (min-width: 750px) { 6 | .articles-wrapper.grid { 7 | margin-bottom: 7rem; 8 | } 9 | } 10 | 11 | .articles-wrapper .article { 12 | max-width: 100%; 13 | } 14 | 15 | @media screen and (max-width: 749px) { 16 | .articles-wrapper .article { 17 | width: 100%; 18 | } 19 | } 20 | 21 | .article { 22 | display: flex; 23 | align-items: center; 24 | } 25 | 26 | .article.grid__item { 27 | padding: 0; 28 | } 29 | 30 | .article-card { 31 | background-color: var(--color-foreground-4); 32 | align-self: flex-start; 33 | flex: 0 1 100%; 34 | display: flex; 35 | align-items: flex-start; 36 | height: 100%; 37 | } 38 | 39 | .grid--peek .article-card { 40 | box-sizing: border-box; 41 | } 42 | 43 | .article-card__info { 44 | padding: 2.5rem 2.5rem 3rem; 45 | display: flex; 46 | flex-direction: column; 47 | flex-grow: 1; 48 | } 49 | 50 | @media screen and (min-width: 750px) { 51 | .article-card__info { 52 | padding: 4rem 5rem; 53 | } 54 | } 55 | 56 | .article-content { 57 | width: 100%; 58 | height: 100%; 59 | display: flex; 60 | flex-direction: column; 61 | text-decoration: none; 62 | color: inherit; 63 | } 64 | 65 | .article-content:hover .article-card__title { 66 | text-decoration: underline; 67 | text-underline-offset: 0.3rem; 68 | } 69 | 70 | .article-card__image { 71 | overflow: hidden; 72 | } 73 | 74 | .article-content img { 75 | transition: transform var(--duration-default) ease; 76 | } 77 | 78 | .article-content:hover img { 79 | transform: scale(1.07); 80 | } 81 | 82 | .article-card__image-wrapper > a { 83 | display: block; 84 | } 85 | 86 | .article-card__title { 87 | text-decoration: none; 88 | word-break: break-word; 89 | } 90 | 91 | .article-card__link.link { 92 | padding: 0; 93 | } 94 | 95 | .article-card__link { 96 | text-underline-offset: 0.3rem; 97 | } 98 | 99 | .article-content:hover .article-card__link { 100 | text-decoration-thickness: 0.2rem; 101 | } 102 | 103 | .article-card__header h2 { 104 | margin: 0; 105 | } 106 | 107 | .article-card__header h2:not(:first-child) { 108 | margin-top: 1rem; 109 | } 110 | 111 | .article-card__footer { 112 | letter-spacing: 0.1rem; 113 | font-size: 1.4rem; 114 | } 115 | 116 | .article-card__footer:not(:last-child) { 117 | margin-bottom: 1rem; 118 | } 119 | 120 | .article-card__footer:last-child { 121 | margin-top: auto; 122 | } 123 | 124 | .article-card__link:not(:only-child) { 125 | margin-right: 3rem; 126 | } 127 | 128 | @media screen and (min-width: 990px) { 129 | .article-card__link:not(:only-child) { 130 | margin-right: 4rem; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /shopify/assets/component-badge.css: -------------------------------------------------------------------------------- 1 | .badge { 2 | border: 1px solid transparent; 3 | border-radius: 4rem; 4 | display: inline-block; 5 | font-size: 1.2rem; 6 | letter-spacing: 0.1rem; 7 | line-height: 1; 8 | padding: 0.6rem 1.6rem; 9 | text-align: center; 10 | background-color: var(--color-badge-background); 11 | border-color: var(--color-badge-border); 12 | color: var(--color-foreground); 13 | word-break: break-word; 14 | } 15 | -------------------------------------------------------------------------------- /shopify/assets/component-cart-notification.css: -------------------------------------------------------------------------------- 1 | .cart-notification-wrapper { 2 | position: relative; 3 | } 4 | 5 | .cart-notification-wrapper .cart-notification { 6 | display: block; 7 | } 8 | 9 | .cart-notification { 10 | background-color: var(--color-background); 11 | border-color: var(--color-foreground-20); 12 | border-style: solid; 13 | border-width: 0 0 0.1rem; 14 | padding: 2.5rem 3.5rem; 15 | position: absolute; 16 | right: 0; 17 | transform: translateY(-100%); 18 | visibility: hidden; 19 | width: 100%; 20 | z-index: -1; 21 | } 22 | 23 | @media screen and (min-width: 750px) { 24 | .cart-notification { 25 | border-width: 0 0.1rem 0.1rem; 26 | max-width: 36.8rem; 27 | right: 4rem; 28 | } 29 | } 30 | 31 | .cart-notification.animate { 32 | transition: transform var(--duration-short) ease, 33 | visibility 0s var(--duration-short) ease; 34 | } 35 | 36 | .cart-notification.active { 37 | transform: translateY(0); 38 | transition: transform var(--duration-default) ease, visibility 0s; 39 | visibility: visible; 40 | } 41 | 42 | .cart-notification__header { 43 | align-items: flex-start; 44 | display: flex; 45 | } 46 | 47 | .cart-notification__heading { 48 | align-items: center; 49 | display: flex; 50 | flex-grow: 1; 51 | margin-bottom: 0; 52 | margin-top: 0; 53 | } 54 | 55 | .cart-notification__heading .icon-checkmark { 56 | color: var(--color-foreground); 57 | margin-right: 1rem; 58 | width: 1.3rem; 59 | } 60 | 61 | .cart-notification__close { 62 | margin-top: -2rem; 63 | margin-right: -3rem; 64 | } 65 | 66 | .cart-notification__links { 67 | text-align: center; 68 | } 69 | 70 | .cart-notification__links > * { 71 | margin-top: 1rem; 72 | } 73 | 74 | .cart-notification-product { 75 | align-items: flex-start; 76 | display: flex; 77 | padding-bottom: 3rem; 78 | padding-top: 2rem; 79 | } 80 | 81 | .cart-notification-product dl { 82 | margin-bottom: 0; 83 | margin-top: 0; 84 | } 85 | 86 | .cart-notification-product__image { 87 | border: 0.1rem solid var(--color-foreground-3); 88 | margin-right: 1.5rem; 89 | } 90 | 91 | .cart-notification-product__name { 92 | margin-bottom: 0; 93 | margin-top: 0; 94 | } 95 | 96 | .cart-notification-product__option { 97 | color: var(--color-foreground-70); 98 | margin-top: 1rem; 99 | } 100 | 101 | .cart-notification-product__option + .cart-notification-product__option { 102 | margin-top: 0.5rem; 103 | } 104 | 105 | .cart-notification-product__option > * { 106 | display: inline-block; 107 | margin: 0; 108 | } 109 | -------------------------------------------------------------------------------- /shopify/assets/component-cart.css: -------------------------------------------------------------------------------- 1 | .cart { 2 | position: relative; 3 | display: block; 4 | } 5 | 6 | .cart__empty-text, 7 | .is-empty .cart__contents, 8 | cart-items.is-empty .title-wrapper-with-link, 9 | .is-empty .cart__footer { 10 | display: none; 11 | } 12 | 13 | .is-empty .cart__empty-text, 14 | .is-empty .cart__warnings { 15 | display: block; 16 | } 17 | 18 | .cart__warnings { 19 | display: none; 20 | text-align: center; 21 | padding: 7rem 0; 22 | } 23 | 24 | .cart__empty-text { 25 | margin: 4.5rem 0 5.5rem; 26 | } 27 | 28 | .cart__contents > * + * { 29 | margin-top: 2.5rem; 30 | } 31 | 32 | @media screen and (min-width: 990px) { 33 | .cart__warnings { 34 | padding: 10rem 0 15rem; 35 | } 36 | 37 | .cart__empty-text { 38 | margin: 5rem 0 6rem; 39 | } 40 | } 41 | 42 | cart-items { 43 | display: block; 44 | } 45 | 46 | .cart__items { 47 | position: relative; 48 | padding-bottom: 3rem; 49 | border-bottom: 0.1rem solid var(--color-foreground-20); 50 | } 51 | 52 | .cart__items--disabled { 53 | pointer-events: none; 54 | } 55 | 56 | .cart__footer { 57 | padding: 4rem 0 0; 58 | } 59 | 60 | .cart__footer-wrapper:last-child .cart__footer { 61 | padding-bottom: 5rem; 62 | } 63 | 64 | .cart__footer > div:only-child { 65 | margin-left: auto; 66 | } 67 | 68 | .cart__footer > * + * { 69 | margin-top: 4rem; 70 | } 71 | 72 | .cart__footer .discounts { 73 | margin-top: 1rem; 74 | } 75 | 76 | .cart__note { 77 | display: block; 78 | } 79 | 80 | .cart__note label { 81 | display: flex; 82 | align-items: flex-end; 83 | line-height: 1; 84 | height: 1.8rem; 85 | margin-bottom: 2rem; 86 | color: var(--color-foreground-75); 87 | } 88 | 89 | .cart__note .field__input { 90 | padding: 1rem; 91 | } 92 | 93 | @media screen and (min-width: 750px) { 94 | .cart__items { 95 | grid-column-start: 1; 96 | grid-column-end: 3; 97 | padding-bottom: 4rem; 98 | margin-bottom: 4rem; 99 | } 100 | 101 | .cart__contents > * + * { 102 | margin-top: 0; 103 | } 104 | 105 | .cart__items + .cart__footer { 106 | grid-column: 2; 107 | } 108 | 109 | .cart__footer { 110 | display: flex; 111 | justify-content: space-between; 112 | border: 0; 113 | } 114 | 115 | .cart__footer-wrapper:last-child { 116 | padding-top: 0; 117 | } 118 | 119 | .cart__footer > * { 120 | width: 35rem; 121 | } 122 | 123 | .cart__footer > * + * { 124 | margin-left: 4rem; 125 | margin-top: 0; 126 | } 127 | } 128 | 129 | .cart__ctas button { 130 | width: 100%; 131 | } 132 | 133 | .cart__ctas > *:not(noscript:first-child) + * { 134 | margin-top: 1rem; 135 | } 136 | 137 | .cart__update-button { 138 | margin-bottom: 1rem; 139 | } 140 | 141 | .cart__dynamic-checkout-buttons { 142 | margin-top: 0; 143 | } 144 | 145 | .cart__dynamic-checkout-buttons div[role='button'] { 146 | border-radius: 0 !important; 147 | } 148 | 149 | .cart-note__label { 150 | display: inline-block; 151 | margin-bottom: 1rem; 152 | line-height: 2; 153 | } 154 | 155 | .tax-note { 156 | margin: 2.2rem 0 1.6rem auto; 157 | text-align: center; 158 | display: block; 159 | } 160 | 161 | .cart__checkout-button { 162 | max-width: 36rem; 163 | } 164 | 165 | .cart__ctas { 166 | text-align: center; 167 | } 168 | 169 | @media screen and (min-width: 750px) { 170 | .cart-note { 171 | max-width: 35rem; 172 | } 173 | 174 | .cart__update-button { 175 | margin-bottom: 0; 176 | margin-right: 0.8rem; 177 | } 178 | 179 | .cart__dynamic-checkout-buttons { 180 | margin-top: 1rem; 181 | } 182 | 183 | .tax-note { 184 | margin-bottom: 2.2rem; 185 | text-align: right; 186 | } 187 | 188 | [data-shopify-buttoncontainer] { 189 | justify-content: flex-end; 190 | } 191 | 192 | .cart__ctas { 193 | display: flex; 194 | gap: 1rem; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /shopify/assets/component-collection-hero.css: -------------------------------------------------------------------------------- 1 | .collection-hero { 2 | margin-bottom: 2rem; 3 | } 4 | 5 | .collection-hero--with-image { 6 | background-color: var(--color-foreground-4); 7 | } 8 | 9 | .collection-hero__inner { 10 | display: flex; 11 | flex-direction: column; 12 | padding-bottom: 2rem; 13 | margin-bottom: 2rem; 14 | } 15 | 16 | @media screen and (min-width: 750px) { 17 | .collection-hero.collection-hero--with-image { 18 | padding: 4rem 0 4rem; 19 | } 20 | } 21 | 22 | .collection-hero__text-wrapper { 23 | flex-basis: 100%; 24 | } 25 | 26 | .collection-hero--with-image .collection-hero__inner { 27 | margin-bottom: 4rem; 28 | } 29 | 30 | @media screen and (min-width: 750px) { 31 | .collection-hero { 32 | padding: 0 0 2rem; 33 | margin-bottom: 0; 34 | } 35 | 36 | .collection-hero--with-image { 37 | margin-bottom: 4.5rem; 38 | } 39 | 40 | .collection-hero__inner { 41 | align-items: center; 42 | flex-direction: row; 43 | padding-bottom: 0; 44 | margin-bottom: 0; 45 | } 46 | 47 | .collection-hero--with-image .collection-hero__inner { 48 | margin-bottom: 0; 49 | } 50 | } 51 | 52 | .collection-hero__title { 53 | margin: 5rem 0 0; 54 | } 55 | 56 | .collection-hero__title + .collection-hero__description { 57 | margin-top: 1.5rem; 58 | font-size: 1.6rem; 59 | line-height: 1.5; 60 | } 61 | 62 | @media screen and (min-width: 750px) { 63 | .collection-hero__title + .collection-hero__description { 64 | font-size: 1.8rem; 65 | margin-top: 2rem; 66 | } 67 | 68 | .collection-hero__description { 69 | max-width: 66.67%; 70 | } 71 | 72 | .collection-hero--with-image .collection-hero__description { 73 | max-width: 100%; 74 | } 75 | } 76 | 77 | .collection-hero--with-image .collection-hero__title { 78 | margin: 0; 79 | } 80 | 81 | .collection-hero--with-image .collection-hero__text-wrapper { 82 | padding: 5rem 0 4rem; 83 | } 84 | 85 | @media screen and (max-width: 749px) { 86 | .collection-hero__image-container { 87 | height: 20rem; 88 | } 89 | } 90 | 91 | @media screen and (min-width: 750px) { 92 | .collection-hero--with-image .collection-hero__text-wrapper { 93 | padding: 4rem 2rem 4rem 0; 94 | flex-basis: 50%; 95 | } 96 | 97 | .collection-hero__image-container { 98 | align-self: stretch; 99 | flex: 1 0 50%; 100 | margin-left: 3rem; 101 | min-height: 20rem; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /shopify/assets/component-deferred-media.css: -------------------------------------------------------------------------------- 1 | .deferred-media__poster { 2 | background-color: transparent; 3 | border: none; 4 | cursor: pointer; 5 | margin: 0; 6 | padding: 0; 7 | height: 100%; 8 | width: 100%; 9 | } 10 | 11 | .media > .deferred-media__poster { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | .deferred-media__poster img { 18 | width: auto; 19 | height: 100%; 20 | } 21 | 22 | .deferred-media { 23 | overflow: hidden; 24 | } 25 | 26 | .deferred-media:not([loaded]) template { 27 | z-index: -1; 28 | } 29 | 30 | .deferred-media[loaded] > .deferred-media__poster { 31 | display: none; 32 | } 33 | 34 | .deferred-media__poster:focus { 35 | outline-offset: -0.3rem; 36 | } 37 | 38 | .deferred-media__poster-button { 39 | background-color: var(--color-background); 40 | border: 0.1rem solid var(--color-foreground-10); 41 | border-radius: 50%; 42 | color: var(--color-foreground); 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | height: 6.2rem; 47 | width: 6.2rem; 48 | position: absolute; 49 | left: 50%; 50 | top: 50%; 51 | transform: translate(-50%, -50%) scale(1); 52 | transition: transform var(--duration-short) ease, color var(--duration-short) ease; 53 | z-index: 1; 54 | } 55 | 56 | .deferred-media__poster-button:hover { 57 | transform: translate(-50%, -50%) scale(1.1); 58 | } 59 | 60 | .deferred-media__poster-button .icon { 61 | width: 2rem; 62 | height: 2rem; 63 | } 64 | 65 | .deferred-media__poster-button .icon-play { 66 | margin-left: 0.2rem; 67 | } 68 | 69 | -------------------------------------------------------------------------------- /shopify/assets/component-discounts.css: -------------------------------------------------------------------------------- 1 | .discounts { 2 | font-size: 1.2rem; 3 | } 4 | 5 | .discounts__discount { 6 | display: flex; 7 | align-items: center; 8 | line-height: 1.5; 9 | } 10 | 11 | .discounts__discount svg { 12 | color: var(--color-button-background); 13 | } 14 | 15 | .discounts__discount--end { 16 | justify-content: flex-end; 17 | } 18 | 19 | .discounts__discount > .icon { 20 | color: var(--color-foreground); 21 | width: 1.2rem; 22 | height: 1.2rem; 23 | margin-right: 0.7rem; 24 | } 25 | -------------------------------------------------------------------------------- /shopify/assets/component-image-with-text.css: -------------------------------------------------------------------------------- 1 | .image-with-text { 2 | margin-top: 5rem; 3 | } 4 | 5 | .image-with-text:not(.color-scheme-background-1) { 6 | margin-bottom: 5rem; 7 | } 8 | 9 | @media screen and (min-width: 750px) { 10 | .image-with-text { 11 | margin-bottom: 5rem; 12 | } 13 | } 14 | 15 | .image-with-text .grid { 16 | margin-left: 0; 17 | margin-bottom: 0; 18 | } 19 | 20 | .image-with-text__grid { 21 | overflow: hidden; 22 | } 23 | 24 | @media screen and (min-width: 750px) { 25 | .image-with-text__grid--reverse { 26 | flex-direction: row-reverse; 27 | } 28 | } 29 | 30 | .image-with-text__media { 31 | background-color: transparent; 32 | min-height: 100%; 33 | } 34 | 35 | .image-with-text__media--small { 36 | height: 19.4rem; 37 | } 38 | 39 | .image-with-text__media--large { 40 | height: 43.5rem; 41 | } 42 | 43 | @media screen and (min-width: 750px) { 44 | .image-with-text__media--small { 45 | height: 31.4rem; 46 | } 47 | 48 | .image-with-text__media--large { 49 | height: 69.5rem; 50 | } 51 | } 52 | 53 | .image-with-text__media--placeholder { 54 | background-color: var(--color-foreground-4); 55 | position: relative; 56 | overflow: hidden; 57 | } 58 | 59 | .image-with-text__media--placeholder.image-with-text__media--adapt { 60 | height: 20rem; 61 | } 62 | 63 | @media screen and (min-width: 750px) { 64 | .image-with-text__media--placeholder.image-with-text__media--adapt { 65 | height: 30rem; 66 | } 67 | } 68 | 69 | .image-with-text__media--placeholder > svg { 70 | position: absolute; 71 | left: 50%; 72 | max-width: 80rem; 73 | top: 50%; 74 | transform: translate(-50%, -50%); 75 | width: 100%; 76 | fill: currentColor; 77 | } 78 | 79 | .image-with-text__content { 80 | display: flex; 81 | flex-direction: column; 82 | align-items: flex-start; 83 | height: 100%; 84 | justify-content: center; 85 | padding: 4rem 4rem 5rem; 86 | } 87 | 88 | @media screen and (min-width: 750px) { 89 | .image-with-text__grid--reverse .image-with-text__content { 90 | margin-left: auto; 91 | } 92 | } 93 | 94 | @media screen and (min-width: 990px) { 95 | .image-with-text__content { 96 | padding: 6rem 7rem 7rem; 97 | } 98 | } 99 | 100 | .image-with-text__content > * + * { 101 | margin-top: 1rem; 102 | } 103 | 104 | .image-with-text__content > .image-with-text__text:empty ~ a { 105 | margin-top: 2rem; 106 | } 107 | 108 | .image-with-text__content > :first-child:is(.image-with-text__heading) { 109 | margin-top: 0; 110 | } 111 | 112 | .image-with-text__content :last-child:is(.image-with-text__heading) { 113 | margin-bottom: 0; 114 | } 115 | 116 | .image-with-text__content :last-child:is(.button) { 117 | margin-top: 2rem; 118 | } 119 | 120 | .image-with-text__content .button + .image-with-text__text { 121 | margin-top: 2rem; 122 | } 123 | 124 | .image-with-text__heading { 125 | margin-bottom: 0; 126 | } 127 | 128 | .image-with-text__text p { 129 | margin-top: 0; 130 | margin-bottom: 1rem; 131 | } 132 | -------------------------------------------------------------------------------- /shopify/assets/component-list-menu.css: -------------------------------------------------------------------------------- 1 | .list-menu--right { 2 | right: 0; 3 | } 4 | 5 | .list-menu--disclosure { 6 | position: absolute; 7 | min-width: 100%; 8 | width: 20rem; 9 | border: 1px solid var(--color-foreground-20); 10 | background-color: var(--color-background); 11 | } 12 | 13 | .list-menu--disclosure:focus { 14 | outline: none; 15 | } 16 | 17 | .list-menu__item--active { 18 | text-decoration: underline; 19 | text-underline-offset: 0.3rem; 20 | } 21 | 22 | .list-menu--disclosure.localization-selector { 23 | max-height: 18rem; 24 | overflow: auto; 25 | width: 10rem; 26 | padding: 0.5rem; 27 | } 28 | -------------------------------------------------------------------------------- /shopify/assets/component-list-payment.css: -------------------------------------------------------------------------------- 1 | .list-payment { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: center; 5 | margin: -0.5rem 0; 6 | padding-top: 1rem; 7 | padding-left: 0; 8 | } 9 | 10 | @media screen and (min-width: 750px) { 11 | .list-payment { 12 | justify-content: flex-end; 13 | margin: -0.5rem; 14 | padding-top: 0; 15 | } 16 | } 17 | 18 | .list-payment__item { 19 | align-items: center; 20 | display: flex; 21 | padding: 0.5rem; 22 | } 23 | -------------------------------------------------------------------------------- /shopify/assets/component-list-social.css: -------------------------------------------------------------------------------- 1 | .list-social { 2 | display: flex; 3 | flex-wrap: wrap; 4 | justify-content: flex-end; 5 | } 6 | 7 | @media only screen and (max-width: 749px) { 8 | .list-social { 9 | justify-content: center; 10 | } 11 | } 12 | 13 | .list-social__item .icon { 14 | height: 1.8rem; 15 | width: 1.8rem; 16 | } 17 | 18 | .list-social__link { 19 | align-items: center; 20 | display: flex; 21 | padding: 1.3rem; 22 | } 23 | 24 | .list-social__link:hover .icon { 25 | transform: scale(1.07); 26 | } 27 | -------------------------------------------------------------------------------- /shopify/assets/component-loading-overlay.css: -------------------------------------------------------------------------------- 1 | .loading-overlay { 2 | position: absolute; 3 | z-index: 1; 4 | width: 3rem; 5 | } 6 | 7 | @media screen and (max-width: 749px) { 8 | .loading-overlay { 9 | top: 0; 10 | right: 0; 11 | } 12 | } 13 | 14 | @media screen and (min-width: 750px) { 15 | .loading-overlay { 16 | left: 0; 17 | } 18 | } 19 | 20 | .loading-overlay__spinner { 21 | width: 3rem; 22 | display: inline-block; 23 | } 24 | 25 | .spinner { 26 | animation: rotator 1.4s linear infinite; 27 | } 28 | 29 | @keyframes rotator { 30 | 0% { 31 | transform: rotate(0deg); 32 | } 33 | 100% { 34 | transform: rotate(270deg); 35 | } 36 | } 37 | 38 | .path { 39 | stroke-dasharray: 280; 40 | stroke-dashoffset: 0; 41 | transform-origin: center; 42 | stroke: var(--color-foreground); 43 | animation: dash 1.4s ease-in-out infinite; 44 | } 45 | 46 | @keyframes dash { 47 | 0% { 48 | stroke-dashoffset: 280; 49 | } 50 | 50% { 51 | stroke-dashoffset: 75; 52 | transform: rotate(135deg); 53 | } 54 | 100% { 55 | stroke-dashoffset: 280; 56 | transform: rotate(450deg); 57 | } 58 | } 59 | 60 | .loading-overlay:not(.hidden) + .cart-item__price-wrapper, 61 | .loading-overlay:not(.hidden) ~ cart-remove-button { 62 | opacity: 50%; 63 | } 64 | 65 | .loading-overlay:not(.hidden) ~ cart-remove-button { 66 | pointer-events: none; 67 | cursor: default; 68 | } 69 | -------------------------------------------------------------------------------- /shopify/assets/component-model-viewer-ui.css: -------------------------------------------------------------------------------- 1 | .shopify-model-viewer-ui .shopify-model-viewer-ui__controls-area { 2 | background: var(--color-background); 3 | border-color: var(--color-foreground-4); 4 | } 5 | 6 | .shopify-model-viewer-ui .shopify-model-viewer-ui__button { 7 | color: var(--color-foreground-75); 8 | } 9 | 10 | .shopify-model-viewer-ui .shopify-model-viewer-ui__button--control:hover { 11 | color: var(--color-foreground-55); 12 | } 13 | 14 | .shopify-model-viewer-ui .shopify-model-viewer-ui__button--control:active, 15 | .shopify-model-viewer-ui .shopify-model-viewer-ui__button--control.focus-visible:focus { 16 | color: var(--color-foreground-55); 17 | background: var(--color-foreground-4); 18 | } 19 | 20 | .shopify-model-viewer-ui .shopify-model-viewer-ui__button--control:not(:last-child):after { 21 | border-color: var(--color-foreground-4); 22 | } 23 | 24 | .shopify-model-viewer-ui .shopify-model-viewer-ui__button--poster { 25 | border-radius: 50%; 26 | color: var(--color-foreground); 27 | background: var(--color-background); 28 | border-color: var(--color-foreground-10); 29 | transform: translate(-50%, -50%) scale(1); 30 | transition: transform var(--duration-short) ease, color var(--duration-short) ease; 31 | } 32 | 33 | .shopify-model-viewer-ui .shopify-model-viewer-ui__poster-control-icon { 34 | width: 4.8rem; 35 | height: 4.8rem; 36 | margin-top: .3rem; 37 | } 38 | 39 | .shopify-model-viewer-ui .shopify-model-viewer-ui__button--poster:hover, 40 | .shopify-model-viewer-ui .shopify-model-viewer-ui__button--poster:focus { 41 | transform: translate(-50%, -50%) scale(1.1); 42 | } 43 | 44 | -------------------------------------------------------------------------------- /shopify/assets/component-newsletter.css: -------------------------------------------------------------------------------- 1 | .newsletter-form { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | width: 100%; 7 | position: relative; 8 | } 9 | 10 | @media screen and (min-width: 750px) { 11 | .newsletter-form { 12 | flex-direction: row; 13 | align-items: flex-start; 14 | margin: 0 auto; 15 | max-width: 50rem; 16 | } 17 | } 18 | 19 | .newsletter-form__field-wrapper { 20 | width: 100%; 21 | } 22 | 23 | .newsletter-form__message { 24 | justify-content: center; 25 | margin-bottom: 0; 26 | } 27 | 28 | .newsletter-form__message--success { 29 | margin-top: 2rem; 30 | } 31 | 32 | @media screen and (min-width: 750px) { 33 | .newsletter-form__message { 34 | justify-content: flex-start; 35 | } 36 | 37 | .newsletter-form__message--success { 38 | position: absolute; 39 | left: 0; 40 | bottom: -65%; 41 | } 42 | } 43 | 44 | .newsletter-form__button { 45 | margin-left: 1.4rem; 46 | } 47 | 48 | @media screen and (max-width: 989px) { 49 | .newsletter-form__button { 50 | width: 100%; 51 | margin: 1.4rem 0 0 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /shopify/assets/component-pagination.css: -------------------------------------------------------------------------------- 1 | .pagination-wrapper { 2 | margin-top: 4rem; 3 | margin-bottom: 7rem; 4 | } 5 | 6 | .pagination-wrapper-small { 7 | margin-top: 1rem; 8 | margin-bottom: 7rem; 9 | } 10 | 11 | @media screen and (min-width: 990px) { 12 | .pagination-wrapper { 13 | margin-top: 5rem; 14 | margin-bottom: 10rem; 15 | } 16 | } 17 | 18 | .pagination__list { 19 | display: flex; 20 | flex-wrap: wrap; 21 | justify-content: center; 22 | } 23 | 24 | .pagination__list > li { 25 | flex: 1 0 4.4rem; 26 | max-width: 4.4rem; 27 | } 28 | 29 | .pagination__list > li:not(:last-child) { 30 | margin-right: 1rem; 31 | } 32 | 33 | .pagination__item { 34 | color: var(--color-foreground); 35 | display: inline-flex; 36 | justify-content: center; 37 | align-items: center; 38 | position: relative; 39 | height: 4.4rem; 40 | width: 100%; 41 | padding: 0; 42 | text-decoration: none; 43 | } 44 | 45 | .pagination__item:hover { 46 | color: var(--color-foreground); 47 | } 48 | 49 | a.pagination__item:hover::after { 50 | height: 0.2rem; 51 | } 52 | 53 | .pagination__item .icon-caret { 54 | height: 0.6rem; 55 | } 56 | 57 | .pagination__item--current { 58 | font-weight: 600; 59 | } 60 | 61 | .pagination__item--current::after { 62 | height: 0.1rem; 63 | } 64 | 65 | .pagination__item--current::after, 66 | .pagination__item:hover::after { 67 | content: ''; 68 | display: block; 69 | width: 2rem; 70 | position: absolute; 71 | bottom: 8px; 72 | left: 50%; 73 | transform: translateX(-50%); 74 | background-color: currentColor; 75 | } 76 | 77 | .pagination__item--next .icon { 78 | margin-left: -0.2rem; 79 | transform: rotate(90deg); 80 | } 81 | 82 | .pagination__item--next:hover .icon { 83 | transform: rotate(90deg) scale(1.07); 84 | } 85 | 86 | .pagination__item--prev .icon { 87 | margin-right: -0.2rem; 88 | transform: rotate(-90deg); 89 | } 90 | 91 | .pagination__item--prev:hover .icon { 92 | transform: rotate(-90deg) scale(1.07); 93 | } 94 | 95 | .pagination__item-arrow { 96 | color: var(--color-foreground-75); 97 | } 98 | 99 | .pagination__item-arrow:hover .icon { 100 | color: var(--color-foreground); 101 | } 102 | 103 | .pagination__item-arrow:hover::after { 104 | display: none; 105 | } 106 | -------------------------------------------------------------------------------- /shopify/assets/component-pickup-availability.css: -------------------------------------------------------------------------------- 1 | pickup-availability { 2 | display: block; 3 | } 4 | 5 | pickup-availability[available] { 6 | min-height: 12rem; 7 | } 8 | 9 | .pickup-availability-preview { 10 | align-items: flex-start; 11 | display: flex; 12 | gap: 0.2rem; 13 | } 14 | 15 | @media screen and (min-width: 750px) { 16 | .pickup-availability-preview { 17 | padding: 0 2rem 0 0; 18 | } 19 | } 20 | 21 | .pickup-availability-preview .icon { 22 | flex-shrink: 0; 23 | height: 1.8rem; 24 | } 25 | 26 | .pickup-availability-preview .icon-unavailable { 27 | height: 1.6rem; 28 | margin-top: 0.1rem; 29 | } 30 | 31 | .pickup-availability-button { 32 | background-color: transparent; 33 | color: var(--color-foreground-75); 34 | letter-spacing: 0.06rem; 35 | padding: 0 0 0.2rem; 36 | text-decoration: underline; 37 | } 38 | 39 | .pickup-availability-button:hover { 40 | color: var(--color-foreground); 41 | } 42 | 43 | .pickup-availability-info * { 44 | margin: 0 0 0.6rem; 45 | } 46 | 47 | pickup-availability-drawer { 48 | background-color: var(--color-background); 49 | border: 0.1rem solid var(--color-foreground-20); 50 | height: 100%; 51 | opacity: 0; 52 | overflow-y: auto; 53 | padding: 2rem; 54 | position: fixed; 55 | top: 0; 56 | right: 0; 57 | z-index: 4; 58 | transition: opacity var(--duration-default) ease, 59 | transform var(--duration-default) ease; 60 | transform: translateX(100%); 61 | width: 100%; 62 | } 63 | 64 | pickup-availability-drawer[open] { 65 | transform: translateX(0); 66 | opacity: 1; 67 | } 68 | 69 | @media screen and (min-width: 750px) { 70 | pickup-availability-drawer { 71 | transform: translateX(100%); 72 | width: 37.5rem; 73 | } 74 | 75 | pickup-availability-drawer[open] { 76 | opacity: 1; 77 | transform: translateX(0); 78 | animation: animateDrawerOpen var(--duration-default) ease; 79 | } 80 | } 81 | 82 | .pickup-availability-header { 83 | align-items: flex-start; 84 | display: flex; 85 | justify-content: space-between; 86 | margin-bottom: 1.2rem; 87 | } 88 | 89 | .pickup-availability-drawer-title { 90 | margin: 0.5rem 0 0; 91 | } 92 | 93 | .pickup-availability-header .icon { 94 | width: 2rem; 95 | } 96 | 97 | .pickup-availability-drawer-button { 98 | background-color: transparent; 99 | border: none; 100 | color: var(--color-base-text); 101 | cursor: pointer; 102 | display: block; 103 | height: 4.4rem; 104 | padding: 1.2rem; 105 | width: 4.4rem; 106 | } 107 | 108 | .pickup-availability-drawer-button:hover { 109 | color: var(--color-foreground-75); 110 | } 111 | 112 | .pickup-availability-variant { 113 | font-size: 1.3rem; 114 | line-height: 1.2; 115 | margin: 0 0 1.2rem; 116 | text-transform: capitalize; 117 | } 118 | 119 | .pickup-availability-variant > * + strong { 120 | margin-left: 1rem; 121 | } 122 | 123 | .pickup-availability-list__item { 124 | border-bottom: 0.1rem solid var(--color-foreground-20); 125 | padding: 2rem 0; 126 | } 127 | 128 | .pickup-availability-list__item:first-child { 129 | border-top: 0.1rem solid var(--color-foreground-20); 130 | } 131 | 132 | .pickup-availability-list__item > * { 133 | margin: 0; 134 | } 135 | 136 | .pickup-availability-list__item > * + * { 137 | margin-top: 1rem; 138 | } 139 | 140 | .pickup-availability-address { 141 | font-style: normal; 142 | font-size: 1.2rem; 143 | line-height: 1.5; 144 | } 145 | 146 | .pickup-availability-address p { 147 | margin: 0; 148 | } 149 | 150 | @keyframes animateDrawerOpen { 151 | @media screen and (max-width: 749px) { 152 | 0% { 153 | opacity: 0; 154 | transform: translateX(100%); 155 | } 156 | 157 | 100% { 158 | opacity: 1; 159 | transform: translateX(0); 160 | } 161 | } 162 | 163 | @media screen and (min-width: 750px) { 164 | 0% { 165 | opacity: 0; 166 | transform: translateX(100%); 167 | } 168 | 169 | 100% { 170 | opacity: 1; 171 | transform: translateX(0); 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /shopify/assets/component-price.css: -------------------------------------------------------------------------------- 1 | .price { 2 | align-items: center; 3 | display: flex; 4 | flex-direction: row; 5 | flex-wrap: wrap; 6 | font-size: 1.6rem; 7 | letter-spacing: 0.1rem; 8 | line-height: 1.5; 9 | color: var(--color-foreground); 10 | } 11 | 12 | .price.price--unavailable { 13 | visibility: hidden; 14 | } 15 | 16 | .price--end { 17 | justify-content: flex-end; 18 | } 19 | 20 | .price dl { 21 | margin: 0; 22 | display: flex; 23 | flex-direction: column; 24 | } 25 | 26 | .price dd { 27 | margin: 0 1rem 0 0; 28 | } 29 | 30 | .price .price__last:last-of-type { 31 | margin: 0; 32 | } 33 | 34 | @media screen and (min-width: 750px) { 35 | .price { 36 | margin-bottom: 0; 37 | } 38 | } 39 | 40 | .price--large { 41 | font-size: 1.6rem; 42 | line-height: 1.5; 43 | letter-spacing: 0.13rem; 44 | } 45 | 46 | @media screen and (min-width: 750px) { 47 | .price--large { 48 | font-size: 1.8rem; 49 | } 50 | } 51 | 52 | .price--sold-out .price__availability, 53 | .price__regular { 54 | display: block; 55 | } 56 | 57 | .price__sale, 58 | .price__availability, 59 | .price .price__badge-sale, 60 | .price .price__badge-sold-out, 61 | .price--on-sale .price__regular, 62 | .price--on-sale .price__availability, 63 | .price--no-compare .price__compare { 64 | display: none; 65 | } 66 | 67 | .price--sold-out .price__badge-sold-out, 68 | .price--on-sale .price__badge-sale { 69 | display: inline-flex; 70 | } 71 | 72 | .price--on-sale .price__sale { 73 | display: flex; 74 | flex-direction: row; 75 | flex-wrap: wrap; 76 | } 77 | 78 | .price--center { 79 | display: flex; 80 | justify-content: center; 81 | } 82 | 83 | .price--on-sale .price-item--regular { 84 | text-decoration: line-through; 85 | color: var(--color-foreground-75); 86 | } 87 | 88 | .unit-price { 89 | font-size: 1.1rem; 90 | letter-spacing: 0.04rem; 91 | line-height: 1.2; 92 | margin-top: 0.2rem; 93 | text-transform: uppercase; 94 | color: var(--color-foreground-70); 95 | } 96 | -------------------------------------------------------------------------------- /shopify/assets/component-product-model.css: -------------------------------------------------------------------------------- 1 | .button.product__xr-button { 2 | background: var(--color-foreground-8); 3 | color: var(--color-foreground); 4 | margin: 1rem auto; 5 | box-shadow: none; 6 | } 7 | 8 | .button.product__xr-button:hover { 9 | box-shadow: none; 10 | } 11 | 12 | .product__xr-button[data-shopify-xr-hidden] { 13 | visibility: hidden; 14 | } 15 | 16 | @media screen and (max-width: 749px) { 17 | slider-component .product__xr-button:not([data-shopify-xr-hidden]) { 18 | display: none; 19 | } 20 | 21 | .active .product__xr-button:not([data-shopify-xr-hidden]) { 22 | display: block; 23 | } 24 | } 25 | 26 | @media screen and (min-width: 750px) { 27 | .product__media-wrapper > .button.product__xr-button { 28 | display: none; 29 | } 30 | 31 | .product__xr-button[data-shopify-xr-hidden] { 32 | display: none; 33 | } 34 | } 35 | 36 | .product__xr-button .icon { 37 | width: 1.4rem; 38 | margin-right: 1rem; 39 | } 40 | -------------------------------------------------------------------------------- /shopify/assets/component-rte.css: -------------------------------------------------------------------------------- 1 | .rte > p:first-child { 2 | margin-top: 0; 3 | } 4 | 5 | .rte > p:last-child { 6 | margin-bottom: 0; 7 | } 8 | 9 | .rte table { 10 | table-layout: fixed; 11 | } 12 | 13 | @media screen and (min-width: 750px) { 14 | .rte table td { 15 | padding-left: 1.2rem; 16 | padding-right: 1.2rem; 17 | } 18 | } 19 | 20 | .rte img { 21 | height: auto; 22 | max-width: 100%; 23 | } 24 | 25 | .rte ul { 26 | padding-left: 2rem; 27 | } 28 | 29 | .rte li { 30 | list-style: inherit; 31 | } 32 | 33 | .rte li:last-child { 34 | margin-bottom: 0; 35 | } 36 | 37 | .rte a { 38 | color: var(--color-link-hover); 39 | text-underline-offset: 0.3rem; 40 | text-decoration-thickness: 0.1rem; 41 | transition: text-decoration-thickness var(--duration-short) ease; 42 | } 43 | 44 | .rte a:hover { 45 | color: var(--color-link); 46 | text-decoration-thickness: 0.2rem; 47 | } 48 | 49 | .rte blockquote { 50 | display: inline-flex; 51 | } 52 | 53 | .rte blockquote > * { 54 | margin: -0.5rem 0 -0.5rem 0; 55 | } 56 | -------------------------------------------------------------------------------- /shopify/assets/component-search.css: -------------------------------------------------------------------------------- 1 | .search__input.field__input { 2 | padding-right: 5rem; 3 | } 4 | 5 | .search__button .icon { 6 | height: 1.8rem; 7 | } 8 | 9 | /* Remove extra spacing for search inputs in Safari */ 10 | input::-webkit-search-decoration { 11 | -webkit-appearance: none; 12 | } 13 | -------------------------------------------------------------------------------- /shopify/assets/component-totals.css: -------------------------------------------------------------------------------- 1 | .totals { 2 | display: flex; 3 | justify-content: center; 4 | align-items: flex-end; 5 | } 6 | 7 | .totals > * { 8 | font-size: 1.6rem; 9 | margin: 0; 10 | } 11 | 12 | .totals * { 13 | line-height: 1; 14 | } 15 | 16 | .totals > * + * { 17 | margin-left: 2rem; 18 | } 19 | 20 | .totals__subtotal-value { 21 | font-size: 1.8rem; 22 | } 23 | 24 | .cart__ctas + .totals { 25 | margin-top: 2rem; 26 | } 27 | 28 | @media all and (min-width: 750px) { 29 | .totals { 30 | justify-content: flex-end; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /shopify/assets/customer.js: -------------------------------------------------------------------------------- 1 | const selectors = { 2 | customerAddresses: '[data-customer-addresses]', 3 | addressCountrySelect: '[data-address-country-select]', 4 | addressContainer: '[data-address]', 5 | toggleAddressButton: 'button[aria-expanded]', 6 | cancelAddressButton: 'button[type="reset"]', 7 | deleteAddressButton: 'button[data-confirm-message]' 8 | }; 9 | 10 | const attributes = { 11 | expanded: 'aria-expanded', 12 | confirmMessage: 'data-confirm-message' 13 | }; 14 | 15 | class CustomerAddresses { 16 | constructor() { 17 | this.elements = this._getElements(); 18 | if (Object.keys(this.elements).length === 0) return; 19 | this._setupCountries(); 20 | this._setupEventListeners(); 21 | } 22 | 23 | _getElements() { 24 | const container = document.querySelector(selectors.customerAddresses); 25 | return container ? { 26 | container, 27 | addressContainer: container.querySelector(selectors.addressContainer), 28 | toggleButtons: document.querySelectorAll(selectors.toggleAddressButton), 29 | cancelButtons: container.querySelectorAll(selectors.cancelAddressButton), 30 | deleteButtons: container.querySelectorAll(selectors.deleteAddressButton), 31 | countrySelects: container.querySelectorAll(selectors.addressCountrySelect) 32 | } : {}; 33 | } 34 | 35 | _setupCountries() { 36 | if (Shopify && Shopify.CountryProvinceSelector) { 37 | // eslint-disable-next-line no-new 38 | new Shopify.CountryProvinceSelector('AddressCountryNew', 'AddressProvinceNew', { 39 | hideElement: 'AddressProvinceContainerNew' 40 | }); 41 | this.elements.countrySelects.forEach((select) => { 42 | const formId = select.dataset.formId; 43 | // eslint-disable-next-line no-new 44 | new Shopify.CountryProvinceSelector(`AddressCountry_${formId}`, `AddressProvince_${formId}`, { 45 | hideElement: `AddressProvinceContainer_${formId}` 46 | }); 47 | }); 48 | } 49 | } 50 | 51 | _setupEventListeners() { 52 | this.elements.toggleButtons.forEach((element) => { 53 | element.addEventListener('click', this._handleAddEditButtonClick); 54 | }); 55 | this.elements.cancelButtons.forEach((element) => { 56 | element.addEventListener('click', this._handleCancelButtonClick); 57 | }); 58 | this.elements.deleteButtons.forEach((element) => { 59 | element.addEventListener('click', this._handleDeleteButtonClick); 60 | }); 61 | } 62 | 63 | _toggleExpanded(target) { 64 | target.setAttribute( 65 | attributes.expanded, 66 | (target.getAttribute(attributes.expanded) === 'false').toString() 67 | ); 68 | } 69 | 70 | _handleAddEditButtonClick = ({ currentTarget }) => { 71 | this._toggleExpanded(currentTarget); 72 | } 73 | 74 | _handleCancelButtonClick = ({ currentTarget }) => { 75 | this._toggleExpanded( 76 | currentTarget 77 | .closest(selectors.addressContainer) 78 | .querySelector(`[${attributes.expanded}]`) 79 | ) 80 | } 81 | 82 | _handleDeleteButtonClick = ({ currentTarget }) => { 83 | // eslint-disable-next-line no-alert 84 | if (confirm(currentTarget.getAttribute(attributes.confirmMessage))) { 85 | Shopify.postLink(currentTarget.dataset.target, { 86 | parameters: { _method: 'delete' }, 87 | }); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /shopify/assets/details-disclosure.js: -------------------------------------------------------------------------------- 1 | class DetailsDisclosure extends HTMLElement { 2 | constructor() { 3 | super(); 4 | this.mainDetailsToggle = this.querySelector('details'); 5 | 6 | this.addEventListener('keyup', this.onKeyUp); 7 | this.mainDetailsToggle.addEventListener('focusout', this.onFocusOut.bind(this)); 8 | } 9 | 10 | onKeyUp(event) { 11 | if(event.code.toUpperCase() !== 'ESCAPE') return; 12 | 13 | const openDetailsElement = event.target.closest('details[open]'); 14 | if (!openDetailsElement) return; 15 | 16 | const summaryElement = openDetailsElement.querySelector('summary'); 17 | openDetailsElement.removeAttribute('open'); 18 | summaryElement.focus(); 19 | } 20 | 21 | onFocusOut() { 22 | setTimeout(() => { 23 | if (!this.contains(document.activeElement)) this.close(); 24 | }) 25 | } 26 | 27 | close() { 28 | this.mainDetailsToggle.removeAttribute('open') 29 | } 30 | } 31 | 32 | customElements.define('details-disclosure', DetailsDisclosure); 33 | -------------------------------------------------------------------------------- /shopify/assets/details-modal.js: -------------------------------------------------------------------------------- 1 | class DetailsModal extends HTMLElement { 2 | constructor() { 3 | super(); 4 | this.detailsContainer = this.querySelector('details'); 5 | this.summaryToggle = this.querySelector('summary'); 6 | 7 | this.detailsContainer.addEventListener( 8 | 'keyup', 9 | (event) => event.code.toUpperCase() === 'ESCAPE' && this.close() 10 | ); 11 | this.summaryToggle.addEventListener( 12 | 'click', 13 | this.onSummaryClick.bind(this) 14 | ); 15 | this.querySelector('button[type="button"]').addEventListener( 16 | 'click', 17 | this.close.bind(this) 18 | ); 19 | 20 | this.summaryToggle.setAttribute('role', 'button'); 21 | this.summaryToggle.setAttribute('aria-expanded', 'false'); 22 | } 23 | 24 | isOpen() { 25 | return this.detailsContainer.hasAttribute('open'); 26 | } 27 | 28 | onSummaryClick(event) { 29 | event.preventDefault(); 30 | event.target.closest('details').hasAttribute('open') 31 | ? this.close() 32 | : this.open(event); 33 | } 34 | 35 | onBodyClick(event) { 36 | if (!this.contains(event.target)) this.close(false); 37 | } 38 | 39 | open(event) { 40 | this.onBodyClickEvent = 41 | this.onBodyClickEvent || this.onBodyClick.bind(this); 42 | event.target.closest('details').setAttribute('open', true); 43 | document.body.addEventListener('click', this.onBodyClickEvent); 44 | 45 | trapFocus( 46 | this.detailsContainer.querySelector('[tabindex="-1"]'), 47 | this.detailsContainer.querySelector('input:not([type="hidden"])') 48 | ); 49 | } 50 | 51 | close(focusToggle = true) { 52 | removeTrapFocus(focusToggle ? this.summaryToggle : null); 53 | this.detailsContainer.removeAttribute('open'); 54 | document.body.removeEventListener('click', this.onBodyClickEvent); 55 | } 56 | } 57 | 58 | customElements.define('details-modal', DetailsModal); 59 | -------------------------------------------------------------------------------- /shopify/assets/disclosure.css: -------------------------------------------------------------------------------- 1 | .disclosure { 2 | position: relative; 3 | } 4 | 5 | .disclosure__button { 6 | align-items: center; 7 | cursor: pointer; 8 | display: flex; 9 | height: 4rem; 10 | padding: 0 1.5rem 0 1.5rem; 11 | font-size: 1.3rem; 12 | background-color: transparent; 13 | } 14 | 15 | .disclosure__list { 16 | border: 1px solid var(--color-foreground-20); 17 | font-size: 1.4rem; 18 | margin-top: -0.5rem; 19 | min-height: 8.2rem; 20 | max-height: 19rem; 21 | max-width: 22rem; 22 | min-width: 12rem; 23 | width: max-content; 24 | overflow-y: auto; 25 | padding-bottom: 0.5rem; 26 | padding-top: 0.5rem; 27 | position: absolute; 28 | bottom: 100%; 29 | transform: translateY(-1rem); 30 | z-index: 2; 31 | background-color: var(--color-background); 32 | } 33 | 34 | .disclosure__item { 35 | position: relative; 36 | } 37 | 38 | .disclosure__link { 39 | display: block; 40 | padding: 0.5rem 2.2rem; 41 | text-decoration: none; 42 | line-height: 1.8; 43 | } 44 | -------------------------------------------------------------------------------- /shopify/assets/newsletter-section.css: -------------------------------------------------------------------------------- 1 | .newsletter--narrow .newsletter__wrapper, 2 | .newsletter:not(.newsletter--narrow) .newsletter__wrapper.color-background-1 { 3 | margin-top: 5rem; 4 | margin-bottom: 5rem; 5 | } 6 | 7 | .newsletter__wrapper:not(.color-background-1) { 8 | padding-top: 5rem; 9 | padding-bottom: 5rem; 10 | } 11 | 12 | .newsletter__wrapper { 13 | padding-right: 4rem; 14 | padding-left: 4rem; 15 | } 16 | 17 | @media screen and (min-width: 750px) { 18 | .newsletter__wrapper { 19 | padding-right: 9rem; 20 | padding-left: 9rem; 21 | } 22 | } 23 | 24 | .newsletter__wrapper > * { 25 | margin-top: 0; 26 | margin-bottom: 0; 27 | } 28 | 29 | .newsletter__wrapper > * + * { 30 | margin-top: 2rem; 31 | } 32 | 33 | .newsletter__wrapper > * + .newsletter-form { 34 | margin-top: 3rem; 35 | } 36 | 37 | .newsletter__subheading { 38 | max-width: 70rem; 39 | margin-left: auto; 40 | margin-right: auto; 41 | } 42 | 43 | .newsletter__wrapper .newsletter-form__field-wrapper { 44 | max-width: 36rem; 45 | } 46 | 47 | .newsletter-form__field-wrapper .newsletter-form__message { 48 | margin-top: 1.5rem; 49 | } 50 | 51 | .newsletter__button { 52 | margin-top: 3rem; 53 | width: fit-content; 54 | } 55 | 56 | @media screen and (min-width: 750px) { 57 | .newsletter__button { 58 | flex-shrink: 0; 59 | margin: 0 0 0 1rem; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /shopify/assets/password-modal.js: -------------------------------------------------------------------------------- 1 | class PasswordModal extends DetailsModal { 2 | constructor() { 3 | super(); 4 | 5 | if (this.querySelector('input[aria-invalid="true"]')) this.open({target: this.querySelector('details')}); 6 | } 7 | } 8 | 9 | customElements.define('password-modal', PasswordModal); 10 | -------------------------------------------------------------------------------- /shopify/assets/pickup-availability.js: -------------------------------------------------------------------------------- 1 | class PickupAvailability extends HTMLElement { 2 | constructor() { 3 | super(); 4 | 5 | if(!this.hasAttribute('available')) return; 6 | 7 | this.errorHtml = this.querySelector('template').content.firstElementChild.cloneNode(true); 8 | this.onClickRefreshList = this.onClickRefreshList.bind(this); 9 | this.fetchAvailability(this.dataset.variantId); 10 | } 11 | 12 | fetchAvailability(variantId) { 13 | const variantSectionUrl = `${this.dataset.baseUrl}variants/${variantId}/?section_id=pickup-availability`; 14 | 15 | fetch(variantSectionUrl) 16 | .then(response => response.text()) 17 | .then(text => { 18 | const sectionInnerHTML = new DOMParser() 19 | .parseFromString(text, 'text/html') 20 | .querySelector('.shopify-section'); 21 | this.renderPreview(sectionInnerHTML); 22 | }) 23 | .catch(e => { 24 | this.querySelector('button')?.removeEventListener('click', this.onClickRefreshList); 25 | this.renderError(); 26 | }); 27 | } 28 | 29 | onClickRefreshList(evt) { 30 | this.fetchAvailability(this.dataset.variantId); 31 | } 32 | 33 | renderError() { 34 | this.innerHTML = ''; 35 | this.appendChild(this.errorHtml); 36 | 37 | this.querySelector('button').addEventListener('click', this.onClickRefreshList); 38 | } 39 | 40 | renderPreview(sectionInnerHTML) { 41 | const drawer = document.querySelector('pickup-availability-drawer'); 42 | if (drawer) drawer.remove(); 43 | if (!sectionInnerHTML.querySelector('pickup-availability-preview')) { 44 | this.innerHTML = ""; 45 | this.removeAttribute('available'); 46 | return; 47 | } 48 | 49 | this.innerHTML = sectionInnerHTML.querySelector('pickup-availability-preview').outerHTML; 50 | this.setAttribute('available', ''); 51 | 52 | document.body.appendChild(sectionInnerHTML.querySelector('pickup-availability-drawer')); 53 | 54 | this.querySelector('button').addEventListener('click', (evt) => { 55 | document.querySelector('pickup-availability-drawer').show(evt.target); 56 | }); 57 | } 58 | } 59 | 60 | customElements.define('pickup-availability', PickupAvailability); 61 | 62 | class PickupAvailabilityDrawer extends HTMLElement { 63 | constructor() { 64 | super(); 65 | 66 | this.onBodyClick = this.handleBodyClick.bind(this); 67 | 68 | this.querySelector('button').addEventListener('click', () => { 69 | this.hide(); 70 | }); 71 | 72 | this.addEventListener('keyup', () => { 73 | if(event.code.toUpperCase() === 'ESCAPE') this.hide(); 74 | }); 75 | } 76 | 77 | handleBodyClick(evt) { 78 | const target = evt.target; 79 | if (target != this && !target.closest('pickup-availability-drawer') && target.id != 'ShowPickupAvailabilityDrawer') { 80 | this.hide(); 81 | } 82 | } 83 | 84 | hide() { 85 | this.removeAttribute('open'); 86 | document.body.removeEventListener('click', this.onBodyClick); 87 | document.body.classList.remove('overflow-hidden'); 88 | removeTrapFocus(this.focusElement); 89 | } 90 | 91 | show(focusElement) { 92 | this.focusElement = focusElement; 93 | this.setAttribute('open', ''); 94 | document.body.addEventListener('click', this.onBodyClick); 95 | document.body.classList.add('overflow-hidden'); 96 | trapFocus(this); 97 | } 98 | } 99 | 100 | customElements.define('pickup-availability-drawer', PickupAvailabilityDrawer); 101 | -------------------------------------------------------------------------------- /shopify/assets/product-form.js: -------------------------------------------------------------------------------- 1 | class ProductForm extends HTMLElement { 2 | constructor() { 3 | super(); 4 | 5 | this.form = this.querySelector('form'); 6 | this.form.addEventListener('submit', this.onSubmitHandler.bind(this)); 7 | this.cartNotification = document.querySelector('cart-notification'); 8 | } 9 | 10 | onSubmitHandler(evt) { 11 | evt.preventDefault(); 12 | this.cartNotification.setActiveElement(document.activeElement); 13 | 14 | const submitButton = this.querySelector('[type="submit"]'); 15 | 16 | submitButton.setAttribute('disabled', true); 17 | submitButton.classList.add('loading'); 18 | 19 | const body = JSON.stringify({ 20 | ...JSON.parse(serializeForm(this.form)), 21 | sections: this.cartNotification.getSectionsToRender().map((section) => section.id), 22 | sections_url: window.location.pathname 23 | }); 24 | 25 | fetch(`${routes.cart_add_url}`, { ...fetchConfig('javascript'), body }) 26 | .then((response) => response.json()) 27 | .then((parsedState) => { 28 | this.cartNotification.renderContents(parsedState); 29 | }) 30 | .catch((e) => { 31 | console.error(e); 32 | }) 33 | .finally(() => { 34 | submitButton.classList.remove('loading'); 35 | submitButton.removeAttribute('disabled'); 36 | }); 37 | } 38 | } 39 | 40 | customElements.define('product-form', ProductForm); 41 | -------------------------------------------------------------------------------- /shopify/assets/product-model.js: -------------------------------------------------------------------------------- 1 | class ProductModel extends DeferredMedia { 2 | constructor() { 3 | super(); 4 | } 5 | 6 | loadContent() { 7 | super.loadContent(); 8 | 9 | Shopify.loadFeatures([ 10 | { 11 | name: 'model-viewer-ui', 12 | version: '1.0', 13 | onLoad: this.setupModelViewerUI.bind(this), 14 | }, 15 | ]); 16 | } 17 | 18 | setupModelViewerUI(errors) { 19 | if (errors) return; 20 | 21 | this.modelViewerUI = new Shopify.ModelViewerUI(this.querySelector('model-viewer')); 22 | } 23 | } 24 | customElements.define('product-model', ProductModel); 25 | 26 | window.ProductModel = { 27 | loadShopifyXR() { 28 | Shopify.loadFeatures([ 29 | { 30 | name: 'shopify-xr', 31 | version: '1.0', 32 | onLoad: this.setupShopifyXR.bind(this), 33 | }, 34 | ]); 35 | }, 36 | 37 | setupShopifyXR(errors) { 38 | if (errors) return; 39 | 40 | if (!window.ShopifyXR) { 41 | document.addEventListener('shopify_xr_initialized', () => 42 | this.setupShopifyXR() 43 | ); 44 | return; 45 | } 46 | 47 | document.querySelectorAll('[id^="ProductJSON-"]').forEach((modelJSON) => { 48 | window.ShopifyXR.addModels(JSON.parse(modelJSON.textContent)); 49 | modelJSON.remove(); 50 | }); 51 | window.ShopifyXR.setupXRElements(); 52 | }, 53 | }; 54 | 55 | window.addEventListener('DOMContentLoaded', () => { window.ProductModel?.loadShopifyXR(); }); 56 | -------------------------------------------------------------------------------- /shopify/assets/section-blog-post.css: -------------------------------------------------------------------------------- 1 | .article-template > *:first-child:not(.article-template__hero-container) { 2 | margin-top: 5rem; 3 | } 4 | 5 | .article-template__hero-container { 6 | max-width: 130rem; 7 | margin: 0 auto; 8 | } 9 | 10 | @media screen and (min-width: 1320px) { 11 | .article-template__hero-container:first-child { 12 | margin-top: 5rem; 13 | } 14 | } 15 | 16 | .article-template__hero-medium { 17 | height: 15.6rem; 18 | } 19 | 20 | .article-template__hero-large { 21 | height: 19rem; 22 | } 23 | 24 | @media screen and (min-width: 750px) and (max-width: 989px) { 25 | .article-template__hero-medium { 26 | height: 34.9rem; 27 | } 28 | 29 | .article-template__hero-large { 30 | height: 42.3rem; 31 | } 32 | } 33 | 34 | @media screen and (min-width: 990px) { 35 | .article-template__hero-medium { 36 | height: 54.5rem; 37 | } 38 | 39 | .article-template__hero-large { 40 | height: 66rem; 41 | } 42 | } 43 | 44 | .article-template header { 45 | margin-top: 4.4rem; 46 | margin-bottom: 2rem; 47 | } 48 | 49 | @media screen and (min-width: 750px) { 50 | .article-template header { 51 | margin-top: 5rem; 52 | } 53 | } 54 | 55 | .article-template__title { 56 | margin: 0; 57 | } 58 | 59 | .article-template__title:not(:only-child) { 60 | margin-bottom: 1rem; 61 | } 62 | 63 | .article-template__link { 64 | font-size: 1.8rem; 65 | display: flex; 66 | justify-content: center; 67 | align-items: center; 68 | text-underline-offset: 0.3rem; 69 | } 70 | 71 | .article-template__link:hover { 72 | text-decoration-thickness: 0.2rem; 73 | } 74 | 75 | .article-template__link svg { 76 | width: 1.5rem; 77 | transform: rotate(180deg); 78 | margin-right: 1rem; 79 | } 80 | 81 | .article-template__content { 82 | margin-top: 3rem; 83 | margin-bottom: 3rem; 84 | } 85 | 86 | .article-template__social-sharing { 87 | display: flex; 88 | flex-direction: column; 89 | align-items: self-end; 90 | margin-top: 3rem; 91 | } 92 | 93 | .article-template__social-sharing .social-sharing { 94 | margin-left: -1.3rem; 95 | } 96 | 97 | .article-template__comment-wrapper { 98 | margin-top: 5rem; 99 | } 100 | 101 | @media screen and (min-width: 750px) { 102 | .article-template__comment-wrapper { 103 | margin-top: 6rem; 104 | } 105 | } 106 | 107 | .article-template__comment-wrapper h2 { 108 | margin-top: 0; 109 | } 110 | 111 | .article-template__comments { 112 | margin-bottom: 5rem; 113 | } 114 | 115 | @media screen and (min-width: 750px) { 116 | .article-template__comments { 117 | margin-bottom: 7rem; 118 | } 119 | } 120 | 121 | .article-template__comments-fields { 122 | margin-bottom: 4rem; 123 | } 124 | 125 | .article-template__comments-comment { 126 | color: var(--color-foreground-75); 127 | background-color: var(--color-background); 128 | margin-bottom: 1.5rem; 129 | padding: 2rem 2rem 1.5rem; 130 | } 131 | 132 | @media screen and (min-width: 750px) { 133 | .article-template__comments-comment { 134 | padding: 2rem 2.5rem; 135 | } 136 | } 137 | 138 | .article-template__comments-comment p { 139 | margin: 0 0 1rem; 140 | } 141 | 142 | .article-template__comment-fields > * { 143 | margin-bottom: 3rem; 144 | } 145 | 146 | @media screen and (min-width: 750px) { 147 | .article-template__comment-fields { 148 | display: grid; 149 | grid-template-columns: repeat(2, 1fr); 150 | grid-column-gap: 4rem; 151 | } 152 | } 153 | 154 | .article-template__comment-warning { 155 | margin: 2rem 0 2.5rem; 156 | } 157 | 158 | @media screen and (min-width: 990px) { 159 | .article-template__comments .pagination-wrapper { 160 | margin: 5rem 0 8rem; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /shopify/assets/section-collection-list.css: -------------------------------------------------------------------------------- 1 | @media screen and (max-width: 749px) { 2 | .collage-section + .collection-list-section .no-heading.no-mobile-link { 3 | margin-top: -7rem; 4 | } 5 | .collage-section + .collection-list-section .no-heading:not(.no-mobile-link) { 6 | margin-top: -1rem; 7 | } 8 | } 9 | 10 | @media screen and (min-width: 749px) { 11 | .collage-section + .collection-list-section .no-heading { 12 | margin-top: -4rem; 13 | } 14 | } 15 | 16 | .collection-list-title { 17 | margin: 0; 18 | } 19 | 20 | @media screen and (max-width: 749px) { 21 | .collection-list-wrapper.page-width { 22 | padding: 0; 23 | } 24 | 25 | .collection-list:not(.slider) { 26 | padding-left: 0; 27 | padding-right: 0; 28 | } 29 | 30 | .collection-list-section .collection-list:not(.slider) { 31 | padding-left: 1.5rem; 32 | padding-right: 1.5rem; 33 | } 34 | } 35 | 36 | @media screen and (max-width: 749px) { 37 | .collection-list-wrapper:not(.no-heading) .title-wrapper-with-link { 38 | margin-top: -1rem; 39 | } 40 | } 41 | 42 | @media screen and (min-width: 750px) { 43 | .collection-list-wrapper.no-heading { 44 | margin-top: 6rem; 45 | } 46 | } 47 | 48 | .collection-list__item:only-child { 49 | max-width: 100%; 50 | width: 100%; 51 | } 52 | 53 | .collection-list__item .card--light-border:hover { 54 | border: 0.1rem solid var(--color-foreground-4); 55 | } 56 | 57 | .collection-list__item:only-child .media { 58 | height: 35rem; 59 | } 60 | 61 | @media screen and (max-width: 749px) { 62 | .collection-list .collection-list__item { 63 | width: calc(100% - 3rem); 64 | } 65 | 66 | .collection-list__item.grid__item { 67 | padding-bottom: 1rem; 68 | } 69 | 70 | .slider.collection-list--1-items { 71 | padding-bottom: 0; 72 | } 73 | } 74 | 75 | .collection-list.negative-margin--small { 76 | margin-bottom: -1rem; 77 | } 78 | 79 | @media screen and (min-width: 750px) and (max-width: 989px) { 80 | .slider.collection-list--1-items, 81 | .slider.collection-list--2-items, 82 | .slider.collection-list--3-items, 83 | .slider.collection-list--4-items { 84 | padding-bottom: 0; 85 | } 86 | } 87 | 88 | @media screen and (min-width: 750px) { 89 | .collection-list__item:only-child > *:not(.card--media) { 90 | height: 320px; 91 | } 92 | 93 | .collection-list__item:only-child .media { 94 | height: 47rem; 95 | } 96 | 97 | .collection-list__item a:hover { 98 | box-shadow: none; 99 | } 100 | 101 | .collection-list.grid--3-col-tablet .grid__item { 102 | max-width: 33.33%; 103 | } 104 | 105 | .collection-list--4-items .grid__item, 106 | .collection-list--7-items .grid__item:nth-child(n + 4), 107 | .collection-list--10-items .grid__item:nth-child(n + 7) { 108 | width: 50%; 109 | } 110 | } 111 | 112 | @media screen and (max-width: 989px) { 113 | .collection-list.slider .collection-list__item { 114 | max-width: 100%; 115 | } 116 | } 117 | 118 | .collection-list__item .card__text, 119 | .collection-list__item .card-colored { 120 | position: relative; 121 | } 122 | -------------------------------------------------------------------------------- /shopify/assets/section-contact-form.css: -------------------------------------------------------------------------------- 1 | .contact img { 2 | max-width: 100%; 3 | } 4 | 5 | .contact .field { 6 | margin-bottom: 1.5rem; 7 | } 8 | 9 | @media screen and (min-width: 750px) { 10 | .contact .field { 11 | margin-bottom: 2rem; 12 | } 13 | } 14 | 15 | .contact__button { 16 | margin-top: 3rem; 17 | } 18 | 19 | @media screen and (min-width: 750px) { 20 | .contact__button { 21 | margin-top: 4rem; 22 | } 23 | } 24 | 25 | @media screen and (min-width: 750px) { 26 | .contact__fields { 27 | display: grid; 28 | grid-template-columns: repeat(2, 1fr); 29 | grid-column-gap: 2rem; 30 | } 31 | } 32 | 33 | .grecaptcha-badge { 34 | visibility: hidden; 35 | } 36 | -------------------------------------------------------------------------------- /shopify/assets/section-featured-blog.css: -------------------------------------------------------------------------------- 1 | .blog:not(.background-secondary) { 2 | margin: 5rem 0; 3 | } 4 | 5 | .blog.background-secondary { 6 | padding: 4rem 0 5rem; 7 | } 8 | 9 | .blog .placeholder { 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | height: 22rem; 14 | text-align: center; 15 | padding: 4rem 2rem 5rem; 16 | margin: 0 2rem; 17 | } 18 | 19 | @media screen and (min-width: 750px) { 20 | .blog .placeholder { 21 | margin: 0; 22 | } 23 | } 24 | 25 | @media screen and (max-width: 749px) { 26 | .blog:not(.no-heading) { 27 | margin-top: -1rem; 28 | } 29 | } 30 | 31 | @media screen and (min-width: 750px) { 32 | .blog.no-heading { 33 | margin-top: 6rem; 34 | } 35 | } 36 | 37 | .background-secondary .title-wrapper-with-link { 38 | margin-top: 0; 39 | } 40 | 41 | .blog__title { 42 | margin: 0; 43 | } 44 | 45 | .blog__posts.articles-wrapper { 46 | margin-bottom: 0; 47 | } 48 | 49 | @media screen and (min-width: 750px) { 50 | .blog__post:only-child { 51 | text-align: center; 52 | } 53 | } 54 | 55 | @media screen and (min-width: 990px) { 56 | .blog__posts.articles-wrapper { 57 | padding-bottom: 0; 58 | } 59 | } 60 | 61 | .blog__posts.articles-wrapper .article { 62 | scroll-snap-align: start; 63 | } 64 | 65 | @media screen and (min-width: 750px) { 66 | .blog__posts .article + .article { 67 | margin-left: 1rem; 68 | } 69 | } 70 | 71 | @media screen and (max-width: 749px) { 72 | .blog__post.article { 73 | width: calc(100% - 3rem); 74 | padding-left: 0.5rem; 75 | } 76 | } 77 | 78 | .background-secondary .article-card { 79 | background-color: var(--color-background); 80 | } 81 | 82 | .blog__button { 83 | margin-top: 3rem; 84 | } 85 | 86 | @media screen and (min-width: 750px) { 87 | .blog__button { 88 | margin-top: 5rem; 89 | } 90 | } 91 | 92 | @media screen and (max-width: 749px) { 93 | .slider.blog__posts--1-items { 94 | padding-bottom: 0; 95 | } 96 | } 97 | 98 | @media screen and (min-width: 750px) and (max-width: 989px) { 99 | .slider.blog__posts--1-items, 100 | .slider.blog__posts--2-items { 101 | padding-bottom: 0; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /shopify/assets/section-image-banner.css: -------------------------------------------------------------------------------- 1 | .banner { 2 | display: flex; 3 | position: relative; 4 | flex-direction: column; 5 | min-height: initial; 6 | } 7 | 8 | @media screen and (max-width: 749px) { 9 | .banner:not(.banner--stacked) { 10 | flex-direction: row; 11 | flex-wrap: wrap; 12 | } 13 | } 14 | 15 | @media screen and (min-width: 750px) { 16 | .banner { 17 | min-height: 72rem; 18 | flex-direction: row; 19 | } 20 | } 21 | 22 | @media screen and (max-width: 749px) { 23 | .banner--stacked { 24 | height: auto; 25 | } 26 | 27 | .banner--stacked .banner__media { 28 | flex-direction: column; 29 | } 30 | } 31 | 32 | .banner__media { 33 | height: 100%; 34 | left: 0; 35 | top: 0; 36 | width: 100%; 37 | position: relative; 38 | } 39 | 40 | .banner__media-half { 41 | width: 50%; 42 | } 43 | 44 | .banner__media-half + .banner__media-half { 45 | right: 0; 46 | left: auto; 47 | } 48 | 49 | @media screen and (max-width: 749px) { 50 | .banner--stacked .banner__media-half { 51 | width: 100%; 52 | } 53 | 54 | .banner--stacked .banner__media-half + .banner__media-half { 55 | order: 1; 56 | } 57 | 58 | .banner:not(.banner--adapt):not(.banner--stacked) > .banner__media { 59 | height: 39rem; 60 | } 61 | } 62 | 63 | @media screen and (min-width: 750px) { 64 | .banner__media { 65 | position: absolute; 66 | height: 100%; 67 | } 68 | } 69 | 70 | .banner--adapt { 71 | height: auto; 72 | } 73 | 74 | @media screen and (max-width: 749px) { 75 | .banner--stacked:not(.banner--adapt) .banner__media { 76 | height: 39rem; 77 | } 78 | 79 | .banner::before { 80 | display: none !important; 81 | } 82 | 83 | .banner--stacked .banner__media-image-half { 84 | width: 100%; 85 | } 86 | } 87 | 88 | .banner__media .placeholder-svg { 89 | position: absolute; 90 | left: 0; 91 | top: 0; 92 | height: 100%; 93 | width: 100%; 94 | } 95 | 96 | .banner__content { 97 | padding: 0; 98 | display: flex; 99 | position: relative; 100 | width: 100%; 101 | justify-content: center; 102 | } 103 | 104 | @media screen and (min-width: 750px) { 105 | .banner__content { 106 | padding-bottom: 5rem; 107 | padding-top: 5rem; 108 | } 109 | } 110 | 111 | .banner__box { 112 | border: 0; 113 | padding: 4rem 3.5rem; 114 | position: relative; 115 | height: fit-content; 116 | align-items: center; 117 | text-align: center; 118 | width: 100%; 119 | } 120 | 121 | .banner__box > * + .banner__buttons { 122 | margin: 0 auto; 123 | margin-top: 2.3rem; 124 | transform: translateX(1rem); 125 | } 126 | 127 | .banner__box > * + .banner__buttons--multiple { 128 | display: flex; 129 | max-width: 45rem; 130 | flex-wrap: wrap; 131 | align-items: baseline; 132 | justify-content: center; 133 | } 134 | 135 | @media screen and (min-width: 750px) { 136 | .banner__box > * + .banner__buttons { 137 | margin-top: 2rem; 138 | } 139 | } 140 | 141 | .banner__content .button + .button { 142 | margin-top: 1.5rem; 143 | } 144 | 145 | .banner__content .button { 146 | height: auto; 147 | margin-right: 2rem; 148 | } 149 | 150 | .banner__box > * + .banner__text { 151 | margin-top: 1.5rem; 152 | } 153 | 154 | @media screen and (min-width: 750px) { 155 | .banner__box > * + .banner__text { 156 | margin-top: 2rem; 157 | } 158 | } 159 | 160 | .banner__box > * + * { 161 | margin-top: 1rem; 162 | } 163 | 164 | .banner__box > *:first-child { 165 | margin-top: 0; 166 | } 167 | 168 | @media screen and (max-width: 749px) { 169 | .banner__content .button { 170 | flex-grow: 1; 171 | } 172 | 173 | .banner--stacked .banner__box { 174 | width: 100%; 175 | } 176 | } 177 | 178 | @media screen and (min-width: 750px) { 179 | .banner__box { 180 | padding: 4rem; 181 | width: 54.8rem; 182 | } 183 | 184 | .banner__box > .banner__buttons:only-child .button { 185 | margin-top: 0; 186 | } 187 | } 188 | 189 | .banner__heading > *, 190 | .banner__text > * { 191 | word-wrap: break-word; 192 | } 193 | 194 | .banner__heading { 195 | margin-bottom: 0; 196 | } 197 | -------------------------------------------------------------------------------- /shopify/assets/section-main-blog.css: -------------------------------------------------------------------------------- 1 | .blog-articles { 2 | display: grid; 3 | grid-gap: 1rem; 4 | } 5 | 6 | @media screen and (min-width: 750px) { 7 | .blog-articles { 8 | grid-template-columns: 1fr 1fr; 9 | } 10 | 11 | .blog-articles > *:first-child, 12 | .blog-articles > *:nth-child(4), 13 | .blog-articles > *:last-child:nth-child(2), 14 | .blog-articles > *:last-child:nth-child(5) { 15 | grid-column: span 2; 16 | text-align: center; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /shopify/assets/section-main-page.css: -------------------------------------------------------------------------------- 1 | .page-title { 2 | margin-top: 0; 3 | } 4 | 5 | .main-page-title { 6 | margin-bottom: 3rem; 7 | } 8 | 9 | @media screen and (min-width: 750px) { 10 | .main-page-title { 11 | margin-bottom: 4rem; 12 | } 13 | } 14 | 15 | .page-placeholder-wrapper { 16 | display: flex; 17 | justify-content: center; 18 | } 19 | 20 | .page-placeholder { 21 | width: 52.5rem; 22 | height: 52.5rem; 23 | } 24 | -------------------------------------------------------------------------------- /shopify/assets/section-product-recommendations.css: -------------------------------------------------------------------------------- 1 | .product-recommendations { 2 | display: block; 3 | } 4 | 5 | .product-recommendations__heading { 6 | margin: 0; 7 | margin-bottom: 3rem; 8 | } 9 | 10 | .product-recommendations .grid__item { 11 | padding-bottom: 0; 12 | } 13 | -------------------------------------------------------------------------------- /shopify/assets/section-rich-text.css: -------------------------------------------------------------------------------- 1 | .rich-text { 2 | margin: auto; 3 | max-width: 110rem; 4 | text-align: center; 5 | /* 1.5rem margin on left & right */ 6 | width: calc(100% - 3rem); 7 | } 8 | 9 | .rich-text.rich-text--full-width { 10 | max-width: initial; 11 | width: 100%; 12 | } 13 | 14 | .rich-text__blocks { 15 | margin: auto; 16 | /* 2.5rem margin on left & right */ 17 | width: calc(100% - 5rem); 18 | } 19 | 20 | .rich-text__blocks * { 21 | overflow-wrap: break-word; 22 | } 23 | 24 | .rich-text--full-width .rich-text__blocks { 25 | /* 4rem (1.5rem + 2.5rem) margin on left & right */ 26 | width: calc(100% - 8rem); 27 | } 28 | 29 | .rich-text:not(.rich-text--full-width), 30 | .rich-text--full-width.color-background-1 { 31 | margin-top: 5rem; 32 | margin-bottom: 5rem; 33 | } 34 | 35 | .rich-text:not(.color-background-1) { 36 | padding-top: 5rem; 37 | padding-bottom: 5rem; 38 | } 39 | 40 | @media screen and (min-width: 750px) { 41 | .rich-text { 42 | /* 5rem margin on left & right */ 43 | width: calc(100% - 10rem); 44 | } 45 | 46 | .rich-text__blocks { 47 | max-width: 50rem; 48 | } 49 | 50 | .rich-text--full-width .rich-text__blocks { 51 | /* 7.5rem (5rem + 2.5rem) margin on left & right */ 52 | width: calc(100% - 15rem); 53 | } 54 | } 55 | 56 | @media screen and (min-width: 990px) { 57 | .rich-text__blocks { 58 | max-width: 78rem; 59 | } 60 | } 61 | 62 | /* Blocks */ 63 | 64 | .rich-text__blocks > * { 65 | margin-top: 0; 66 | margin-bottom: 0; 67 | } 68 | 69 | .rich-text__blocks > * + * { 70 | margin-top: 2rem; 71 | } 72 | 73 | .rich-text__blocks > * + a { 74 | margin-top: 3rem; 75 | } 76 | -------------------------------------------------------------------------------- /shopify/assets/share.js: -------------------------------------------------------------------------------- 1 | class ShareButton extends DetailsDisclosure { 2 | constructor() { 3 | super(); 4 | 5 | this.elements = { 6 | shareButton: this.querySelector('button'), 7 | successMessage: this.querySelector('[id^="ShareMessage"]'), 8 | urlInput: this.querySelector('input') 9 | } 10 | if (navigator.share) { 11 | this.mainDetailsToggle.setAttribute('hidden', ''); 12 | this.elements.shareButton.classList.remove('hidden'); 13 | this.elements.shareButton.addEventListener('click', () => { navigator.share({ url: document.location.href, title: document.title }) }); 14 | } else { 15 | this.mainDetailsToggle.addEventListener('toggle', this.toggleDetails.bind(this)); 16 | this.mainDetailsToggle.querySelector('button').addEventListener('click', this.copyToClipboard.bind(this)); 17 | } 18 | } 19 | 20 | toggleDetails() { 21 | if (!this.mainDetailsToggle.open) 22 | this.elements.successMessage.classList.add('hidden'); 23 | } 24 | 25 | copyToClipboard() { 26 | navigator.clipboard.writeText(this.elements.urlInput.value).then(() => { 27 | this.elements.successMessage.classList.remove('hidden'); 28 | this.elements.successMessage.setAttribute('aria-hidden', false); 29 | 30 | setTimeout(() => { 31 | this.elements.successMessage.setAttribute('aria-hidden', true); 32 | }, 6000); 33 | }); 34 | } 35 | } 36 | 37 | customElements.define('share-button', ShareButton); 38 | -------------------------------------------------------------------------------- /shopify/assets/slider.js: -------------------------------------------------------------------------------- 1 | class SliderComponent extends HTMLElement { 2 | constructor() { 3 | super(); 4 | this.slider = this.querySelector('ul'); 5 | this.sliderItems = this.querySelectorAll('li'); 6 | this.pageCount = this.querySelector('.slider-counter--current'); 7 | this.pageTotal = this.querySelector('.slider-counter--total'); 8 | this.prevButton = this.querySelector('button[name="previous"]'); 9 | this.nextButton = this.querySelector('button[name="next"]'); 10 | 11 | if (!this.slider || !this.nextButton) return; 12 | 13 | const resizeObserver = new ResizeObserver(entries => this.initPages()); 14 | resizeObserver.observe(this.slider); 15 | 16 | this.slider.addEventListener('scroll', this.update.bind(this)); 17 | this.prevButton.addEventListener('click', this.onButtonClick.bind(this)); 18 | this.nextButton.addEventListener('click', this.onButtonClick.bind(this)); 19 | } 20 | 21 | initPages() { 22 | if (!this.sliderItems.length === 0) return; 23 | this.slidesPerPage = Math.floor(this.slider.clientWidth / this.sliderItems[0].clientWidth); 24 | this.totalPages = this.sliderItems.length - this.slidesPerPage + 1; 25 | this.update(); 26 | } 27 | 28 | update() { 29 | if (!this.pageCount || !this.pageTotal) return; 30 | this.currentPage = Math.round(this.slider.scrollLeft / this.sliderItems[0].clientWidth) + 1; 31 | 32 | if (this.currentPage === 1) { 33 | this.prevButton.setAttribute('disabled', true); 34 | } else { 35 | this.prevButton.removeAttribute('disabled'); 36 | } 37 | 38 | if (this.currentPage === this.totalPages) { 39 | this.nextButton.setAttribute('disabled', true); 40 | } else { 41 | this.nextButton.removeAttribute('disabled'); 42 | } 43 | 44 | this.pageCount.textContent = this.currentPage; 45 | this.pageTotal.textContent = this.totalPages; 46 | } 47 | 48 | onButtonClick(event) { 49 | event.preventDefault(); 50 | const slideScrollPosition = event.currentTarget.name === 'next' ? this.slider.scrollLeft + this.sliderItems[0].clientWidth : this.slider.scrollLeft - this.sliderItems[0].clientWidth; 51 | this.slider.scrollTo({ 52 | left: slideScrollPosition 53 | }); 54 | } 55 | } 56 | 57 | customElements.define('slider-component', SliderComponent); 58 | -------------------------------------------------------------------------------- /shopify/config/settings_data.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /shopify/sections/announcement-bar.liquid: -------------------------------------------------------------------------------- 1 | {%- for block in section.blocks -%} 2 | {%- case block.type -%} 3 | {%- when 'announcement' -%} 4 | 20 | {%- endcase -%} 21 | {%- endfor -%} 22 | 23 | {% schema %} 24 | { 25 | "name": "t:sections.announcement-bar.name", 26 | "max_blocks": 12, 27 | "blocks": [ 28 | { 29 | "type": "announcement", 30 | "name": "t:sections.announcement-bar.blocks.announcement.name", 31 | "settings": [ 32 | { 33 | "type": "text", 34 | "id": "text", 35 | "default": "Welcome to our store", 36 | "label": "t:sections.announcement-bar.blocks.announcement.settings.text.label" 37 | }, 38 | { 39 | "type": "select", 40 | "id": "color_scheme", 41 | "options": [ 42 | { 43 | "value": "background-1", 44 | "label": "t:sections.announcement-bar.blocks.announcement.settings.color_scheme.options__1.label" 45 | }, 46 | { 47 | "value": "background-2", 48 | "label": "t:sections.announcement-bar.blocks.announcement.settings.color_scheme.options__2.label" 49 | }, 50 | { 51 | "value": "inverse", 52 | "label": "t:sections.announcement-bar.blocks.announcement.settings.color_scheme.options__3.label" 53 | }, 54 | { 55 | "value": "accent-1", 56 | "label": "t:sections.announcement-bar.blocks.announcement.settings.color_scheme.options__4.label" 57 | }, 58 | { 59 | "value": "accent-2", 60 | "label": "t:sections.announcement-bar.blocks.announcement.settings.color_scheme.options__5.label" 61 | } 62 | ], 63 | "default": "accent-1", 64 | "label": "t:sections.announcement-bar.blocks.announcement.settings.color_scheme.label" 65 | }, 66 | { 67 | "type": "url", 68 | "id": "link", 69 | "label": "t:sections.announcement-bar.blocks.announcement.settings.link.label" 70 | } 71 | ] 72 | } 73 | ], 74 | "default": { 75 | "blocks": [ 76 | { 77 | "type": "announcement" 78 | } 79 | ] 80 | } 81 | } 82 | {% endschema %} 83 | -------------------------------------------------------------------------------- /shopify/sections/apps.liquid: -------------------------------------------------------------------------------- 1 |
2 | {%- for block in section.blocks -%} 3 | {% render block %} 4 | {%- endfor -%} 5 |
6 | 7 | {% schema %} 8 | { 9 | "name": "t:sections.apps.name", 10 | "tag": "section", 11 | "class": "spaced-section", 12 | "settings": [ 13 | { 14 | "type": "checkbox", 15 | "id": "include_margins", 16 | "default": true, 17 | "label": "t:sections.apps.settings.include_margins.label" 18 | } 19 | ], 20 | "blocks": [ 21 | { 22 | "type": "@app" 23 | } 24 | ], 25 | "presets": [ 26 | { 27 | "name": "t:sections.apps.presets.name" 28 | } 29 | ] 30 | } 31 | {% endschema %} 32 | -------------------------------------------------------------------------------- /shopify/sections/cart-icon-bubble.liquid: -------------------------------------------------------------------------------- 1 | {%- liquid 2 | if cart == empty 3 | render 'icon-cart-empty' 4 | else 5 | render 'icon-cart' 6 | endif 7 | -%} 8 | {{ 'templates.cart.cart' | t }} 9 | {%- if cart != empty -%} 10 |
11 | {%- if cart.item_count < 100 -%} 12 | 13 | {%- endif -%} 14 | {{ 'sections.header.cart_count' | t: count: cart.item_count }} 15 |
16 | {%- endif -%} 17 | -------------------------------------------------------------------------------- /shopify/sections/cart-live-region-text.liquid: -------------------------------------------------------------------------------- 1 | {{ 'sections.cart.new_subtotal' | t }}: {{ cart.total_price | money_with_currency }} 2 | -------------------------------------------------------------------------------- /shopify/sections/cart-notification-button.liquid: -------------------------------------------------------------------------------- 1 | {{ 'general.cart.view' | t: count: cart.item_count }} 2 | -------------------------------------------------------------------------------- /shopify/sections/cart-notification-product.liquid: -------------------------------------------------------------------------------- 1 | {%- if cart != empty -%} 2 | {%- for item in cart.items -%} 3 |
4 | {% if item.image %} 5 | {{ item.image.alt | escape }} 12 | {% endif %} 13 |
14 |

{{ item.product.title | escape }}

15 | {%- unless item.product.has_only_default_variant -%} 16 |
17 | {%- for option in item.options_with_values -%} 18 |
19 |
{{ option.name }}:
20 |
{{ option.value }}
21 |
22 | {%- endfor -%} 23 |
24 | {%- endunless -%} 25 |
26 |
27 | {%- endfor -%} 28 | {%- endif -%} 29 | -------------------------------------------------------------------------------- /shopify/sections/contact-form.liquid: -------------------------------------------------------------------------------- 1 | {{ 'section-contact-form.css' | asset_url | stylesheet_tag }} 2 | 3 |
4 | {%- form 'contact', id: 'ContactForm' -%} 5 | {%- if form.posted_successfully? -%} 6 |
{% render 'icon-success' %} {{ 'templates.contact.form.post_success' | t }}
7 | {%- elsif form.errors -%} 8 |
9 | 10 |
11 | 18 | {%- endif -%} 19 |
20 |
21 | 22 | 23 |
24 |
25 | 41 | 42 | {%- if form.errors contains 'email' -%} 43 | 44 | {{ 'accessibility.error' | t }} 45 | {% render 'icon-error' %}{{ form.errors.translated_fields['email'] | capitalize }} {{ form.errors.messages['email'] }} 46 | 47 | {%- endif -%} 48 |
49 |
50 |
51 | 52 | 53 |
54 |
55 | 64 | 65 |
66 |
67 | 70 |
71 | {%- endform -%} 72 |
73 | 74 | {% schema %} 75 | { 76 | "name": "t:sections.contact-form.name", 77 | "tag": "section", 78 | "class": "spaced-section", 79 | "presets": [ 80 | { 81 | "name": "t:sections.contact-form.presets.name" 82 | } 83 | ] 84 | } 85 | {% endschema %} 86 | -------------------------------------------------------------------------------- /shopify/sections/custom-liquid.liquid: -------------------------------------------------------------------------------- 1 | {{ section.settings.custom_liquid }} 2 | 3 | {% schema %} 4 | { 5 | "name": "t:sections.custom-liquid.name", 6 | "tag": "section", 7 | "class": "spaced-section", 8 | "settings": [ 9 | { 10 | "type": "liquid", 11 | "id": "custom_liquid", 12 | "label": "t:sections.custom-liquid.settings.custom_liquid.label" 13 | } 14 | ], 15 | "presets": [ 16 | { 17 | "name": "t:sections.custom-liquid.presets.name" 18 | } 19 | ] 20 | } 21 | {% endschema %} 22 | -------------------------------------------------------------------------------- /shopify/sections/main-404.liquid: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |

15 | {{ 'templates.404.subtext' | t }} 16 |

17 |

18 | {{ 'templates.404.title' | t }} 19 |

20 | 21 | {{ 'general.continue_shopping' | t }} 22 | 23 |
24 | -------------------------------------------------------------------------------- /shopify/sections/main-blog.liquid: -------------------------------------------------------------------------------- 1 | {{ 'component-article-card.css' | asset_url | stylesheet_tag }} 2 | {{ 'component-card.css' | asset_url | stylesheet_tag }} 3 | {{ 'section-main-blog.css' | asset_url | stylesheet_tag }} 4 | 5 | 6 | {%- paginate blog.articles by 6 -%} 7 | 8 |
9 |

{{ blog.title | escape }}

10 | 11 |
12 | {%- for article in blog.articles -%} 13 |
14 | {%- render 'article-card', article: article, show_image: section.settings.show_image -%} 15 |
16 | {%- endfor -%} 17 |
18 | 19 | {%- if paginate.pages > 1 -%} 20 | {%- render 'pagination', paginate: paginate -%} 21 | {%- endif -%} 22 |
23 | {%- endpaginate -%} 24 | 25 | {% schema %} 26 | { 27 | "name": "t:sections.main-blog.name", 28 | "tag": "section", 29 | "class": "spaced-section", 30 | "settings": [ 31 | { 32 | "type": "header", 33 | "content": "t:sections.main-blog.settings.header.content" 34 | }, 35 | { 36 | "type": "checkbox", 37 | "id": "show_image", 38 | "default": true, 39 | "label": "t:sections.main-blog.settings.show_image.label", 40 | "info": "t:sections.main-blog.settings.show_image.info" 41 | }, 42 | { 43 | "type": "paragraph", 44 | "content": "t:sections.main-blog.settings.paragraph.content" 45 | } 46 | ], 47 | "blocks": [ 48 | { 49 | "type": "title", 50 | "name": "t:sections.main-blog.blocks.title.name", 51 | "limit": 1, 52 | "settings": [ 53 | { 54 | "type": "checkbox", 55 | "id": "show_date", 56 | "default": true, 57 | "label": "t:sections.main-blog.blocks.title.settings.show_date.label" 58 | }, 59 | { 60 | "type": "checkbox", 61 | "id": "show_author", 62 | "default": false, 63 | "label": "t:sections.main-blog.blocks.title.settings.show_author.label" 64 | } 65 | ] 66 | }, 67 | { 68 | "type": "summary", 69 | "name": "t:sections.main-blog.blocks.summary.name", 70 | "limit": 1 71 | }, 72 | { 73 | "type": "link", 74 | "name": "t:sections.main-blog.blocks.link.name", 75 | "limit": 1 76 | } 77 | ] 78 | } 79 | {% endschema %} 80 | -------------------------------------------------------------------------------- /shopify/sections/main-collection-banner.liquid: -------------------------------------------------------------------------------- 1 | {{ 'component-collection-hero.css' | asset_url | stylesheet_tag }} 2 | 3 |
4 |
5 |
6 |

7 | {{ 'sections.collection_template.title' | t }}: 8 | {{- collection.title | escape -}} 9 |

10 | 11 | {%- if section.settings.show_collection_description -%} 12 |
{{ collection.description }}
13 | {%- endif -%} 14 |
15 | 16 | {%- if section.settings.show_collection_image and collection.image -%} 17 |
18 | {{ collection.title | escape }} 31 |
32 | {%- endif -%} 33 |
34 |
35 | 36 | {% schema %} 37 | { 38 | "name": "t:sections.main-collection-banner.name", 39 | "class": "spaced-section spaced-section--full-width", 40 | "settings": [ 41 | { 42 | "type": "paragraph", 43 | "content": "t:sections.main-collection-banner.settings.paragraph.content" 44 | }, 45 | { 46 | "type": "checkbox", 47 | "id": "show_collection_description", 48 | "default": false, 49 | "label": "t:sections.main-collection-banner.settings.show_collection_description.label" 50 | }, 51 | { 52 | "type": "checkbox", 53 | "id": "show_collection_image", 54 | "default": false, 55 | "label": "t:sections.main-collection-banner.settings.show_collection_image.label", 56 | "info": "t:sections.main-collection-banner.settings.show_collection_image.info" 57 | } 58 | ] 59 | } 60 | {% endschema %} 61 | -------------------------------------------------------------------------------- /shopify/sections/main-page.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |

9 | {{ page.title | escape }} 10 |

11 |
12 | {{ page.content }} 13 |
14 |
15 | 16 | {% schema %} 17 | { 18 | "name": "t:sections.main-page.name", 19 | "tag": "section", 20 | "class": "spaced-section" 21 | } 22 | {% endschema %} 23 | -------------------------------------------------------------------------------- /shopify/sections/page.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |

9 | {%- if section.settings.page.title != blank -%} 10 | {{ section.settings.page.title | escape }} 11 | {%- else -%} 12 | Page title 13 | {%- endif -%} 14 |

15 |
16 | {%- if section.settings.page.content != blank -%} 17 | {{ section.settings.page.content }} 18 | {%- else -%} 19 |
20 | {{ 'page' | placeholder_svg_tag: 'page-placeholder' }} 21 |
22 | {%- endif -%} 23 |
24 |
25 | 26 | {% schema %} 27 | { 28 | "name": "t:sections.page.name", 29 | "tag": "section", 30 | "class": "spaced-section", 31 | "settings": [ 32 | { 33 | "type": "page", 34 | "id": "page", 35 | "label": "t:sections.page.settings.page.label" 36 | } 37 | ], 38 | "presets": [ 39 | { 40 | "name": "t:sections.page.presets.name" 41 | } 42 | ] 43 | } 44 | {% endschema %} 45 | -------------------------------------------------------------------------------- /shopify/sections/pickup-availability.liquid: -------------------------------------------------------------------------------- 1 | {% comment %}theme-check-disable UndefinedObject{% endcomment %} 2 | {%- assign pick_up_availabilities = product_variant.store_availabilities | where: 'pick_up_enabled', true -%} 3 | 4 | {%- if pick_up_availabilities.size > 0 -%} 5 | 6 | {%- liquid 7 | assign closest_location = pick_up_availabilities.first 8 | 9 | if closest_location.available 10 | render 'icon-tick' 11 | endif 12 | -%} 13 | 14 |
15 | {%- if closest_location.available -%} 16 |

{{ 'products.product.pickup_availability.pick_up_available_at_html' | t: location_name: closest_location.location.name }}

17 |

{{ closest_location.pick_up_time }}

18 | 25 | {%- else -%} 26 |

{{ 'products.product.pickup_availability.pick_up_unavailable_at_html' | t: location_name: closest_location.location.name }}

27 | {%- if pick_up_availabilities.size > 1 -%} 28 | 29 | {%- endif -%} 30 | {%- endif -%} 31 |
32 |
33 | 34 | 35 |
36 |

{{ product_variant.product.title | escape }}

37 | 38 |
39 | 40 | {%- unless product_variant.product.has_only_default_variant -%} 41 |

42 | {%- for product_option in product_variant.product.options_with_values -%} 43 | {{ product_option.name | escape }}:  44 | {%- for value in product_option.values -%} 45 | {%- if product_option.selected_value == value -%} 46 | {{ value | escape }} 47 | {%- endif -%} 48 | {%- endfor -%} 49 | {%- unless forloop.last -%}, {%- endunless forloop.last -%} 50 | {%- endfor -%} 51 |

52 | {%- endunless -%} 53 | 54 |
    55 | {%- for availability in pick_up_availabilities -%} 56 |
  • 57 |

    {{ availability.location.name | escape }}

    58 |

    59 | {%- if availability.available -%} 60 | {% render 'icon-tick' %} {{ 'products.product.pickup_availability.pick_up_available' | t }}, {{ availability.pick_up_time | downcase }} 61 | {%- endif -%} 62 |

    63 | 64 | {%- assign address = availability.location.address -%} 65 |
    66 | {{ address | format_address }} 67 | 68 | {%- if address.phone.size > 0 -%} 69 |

    {{ address.phone }}

    70 | {%- endif -%} 71 |
    72 |
  • 73 | {%- endfor -%} 74 |
75 |
76 | {%- endif -%} 77 | -------------------------------------------------------------------------------- /shopify/sections/test/Test.ts: -------------------------------------------------------------------------------- 1 | // Something 2 | -------------------------------------------------------------------------------- /shopify/sections/test/_test.scss: -------------------------------------------------------------------------------- 1 | // Something 2 | -------------------------------------------------------------------------------- /shopify/snippets/article-card.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | Renders an article card for a given blog with settings to either show the image or not. 3 | 4 | Accepts: 5 | - blog: {Object} Blog object 6 | - article: {Object} Article object 7 | - show_image: {String} The setting either show the article image or not. If it's not included it will show the image by default 8 | 9 | Usage: 10 | {% render 'article-card' blog: blog, article: article, show_image: section.settings.show_image %} 11 | {% endcomment %} 12 | 13 |
14 | 15 | {%- if show_image == true and article.image -%} 16 |
17 |
18 | {{ article.image.src.alt | escape }} 33 |
34 |
35 | {%- endif -%} 36 | 37 |
38 | {%- for block in section.blocks -%} 39 | {%- case block.type -%} 40 | {%- when 'title'-%} 41 |
42 |

43 | {{ article.title | escape }} 44 |

45 | {%- if block.settings.show_date -%} 46 | 47 | {{- article.published_at | time_tag: format: 'month_year' -}} 48 | 49 | {%- endif -%} 50 | {%- if block.settings.show_author -%} 51 | {{ article.author -}} 52 | {%- endif -%} 53 |
54 | 55 | {%- when 'summary'-%} 56 | {%- if article.excerpt.size > 0 or article.content.size > 0 -%} 57 |

58 | {%- if article.excerpt.size > 0 -%} 59 | {{ article.excerpt | strip_html | truncatewords: 30 }} 60 | {%- else -%} 61 | {{ article.content | strip_html | truncatewords: 30 }} 62 | {%- endif -%} 63 |

64 | {%- endif -%} 65 | 66 | {%- when 'link'-%} 67 |
68 | 69 | {{ 'blogs.article.read_more' | t }} 70 | 71 | 72 | {%- if article.comments_count > 0 and blog.comments_enabled? -%} 73 | {{ 'blogs.article.comments' | t: count: article.comments_count }} 74 | {%- endif -%} 75 |
76 | {%- endcase -%} 77 | {%- endfor -%} 78 |
79 |
80 |
81 | -------------------------------------------------------------------------------- /shopify/snippets/cart-notification.liquid: -------------------------------------------------------------------------------- 1 | 2 |
3 | 19 |
20 |
21 | {% style %} 22 | .cart-notification { 23 | display: none; 24 | } 25 | {% endstyle %} 26 | -------------------------------------------------------------------------------- /shopify/snippets/icon-3d-model.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-account.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-arrow.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-caret.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-cart-empty.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-cart.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-checkmark.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-clipboard.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-close-small.liquid: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /shopify/snippets/icon-close.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-discount.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-error.liquid: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /shopify/snippets/icon-facebook.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-filter.liquid: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /shopify/snippets/icon-hamburger.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-instagram.liquid: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /shopify/snippets/icon-minus.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-padlock.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-pinterest.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-play.liquid: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /shopify/snippets/icon-plus.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-remove.liquid: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /shopify/snippets/icon-share.liquid: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /shopify/snippets/icon-snapchat.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-success.liquid: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /shopify/snippets/icon-tick.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-tiktok.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-tumblr.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-twitter.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-unavailable.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-vimeo.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-youtube.liquid: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shopify/snippets/icon-zoom.liquid: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /shopify/snippets/meta-tags.liquid: -------------------------------------------------------------------------------- 1 | {%- liquid 2 | assign og_title = page_title | default: shop.name 3 | assign og_url = canonical_url | default: shop.url 4 | assign og_type = 'website' 5 | assign og_description = page_description | default: shop.description | default: shop.name 6 | 7 | if request.page_type == 'product' 8 | assign og_type = 'product' 9 | elsif request.page_type == 'article' 10 | assign og_type = 'article' 11 | elsif request.page_type == 'collection' 12 | assign og_type = 'product.group' 13 | elsif request.page_type == 'password' 14 | assign og_url = shop.url 15 | endif 16 | %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {%- if page_image -%} 25 | 26 | 27 | 28 | 29 | {%- endif -%} 30 | 31 | {%- if request.page_type == 'product' -%} 32 | 33 | 34 | {%- endif -%} 35 | 36 | {%- if settings.social_twitter_link != blank -%} 37 | 38 | {%- endif -%} 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /shopify/snippets/pagination.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | Renders a set of links for paginated results. Must be used within paginate tags. 3 | 4 | Usage: 5 | {% paginate results by 2 %} 6 | {% render 'pagination', paginate: paginate, anchor: '#yourID' %} 7 | {% endpaginate %} 8 | 9 | Accepts: 10 | - paginate: {Object} 11 | - anchor: {String} (optional) This can be added so that on page reload it takes you to wherever you've placed your anchor tag. 12 | - class: {String} (optional) Appended to container element's class attribute 13 | {% endcomment %} 14 | 15 | 16 | 17 | 18 | {%- if paginate.parts.size > 0 -%} 19 |
20 | 53 |
54 | {%- endif -%} 55 | -------------------------------------------------------------------------------- /shopify/snippets/price.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | Renders a list of product's price (regular, sale) 3 | 4 | Accepts: 5 | - product: {Object} Product Liquid object (optional) 6 | - use_variant: {Boolean} Renders selected or first variant price instead of overall product pricing (optional) 7 | - show_badges: {Boolean} Renders 'Sale' and 'Sold Out' tags if the product matches the condition (optional) 8 | - price_class: {String} Adds a price class to the price element (optional) 9 | 10 | Usage: 11 | {% render 'price', product: product %} 12 | {% endcomment %} 13 | {%- liquid 14 | if use_variant 15 | assign target = product.selected_or_first_available_variant 16 | else 17 | assign target = product 18 | endif 19 | 20 | assign compare_at_price = target.compare_at_price 21 | assign price = target.price | default: 1999 22 | assign available = target.available | default: false 23 | assign money_price = price | money 24 | 25 | if target == product and product.price_varies 26 | assign money_price = 'products.product.price.from_price_html' | t: price: money_price 27 | endif 28 | -%} 29 | 30 |
35 |
36 | {%- comment -%} 37 | Explanation of description list: 38 | - div.price__regular: Displayed when there are no variants on sale 39 | - div.price__sale: Displayed when a variant is a sale 40 | - div.price__availability: Displayed when the product is sold out 41 | {%- endcomment -%} 42 |
43 |
44 | {{ 'products.product.price.regular_price' | t }} 45 |
46 |
47 | 48 | {{ money_price }} 49 | 50 |
51 |
52 |
53 |
54 | {{ 'products.product.price.regular_price' | t }} 55 |
56 |
57 | 58 | {{ compare_at_price | money }} 59 | 60 |
61 |
62 | {{ 'products.product.price.sale_price' | t }} 63 |
64 |
65 | 66 | {{ money_price }} 67 | 68 |
69 |
70 | 71 |
{{ 'products.product.price.unit_price' | t }}
72 |
73 | {{- product.selected_or_first_available_variant.unit_price | money -}} 74 | 75 |  {{ 'accessibility.unit_price_separator' | t }}  76 | 77 | {%- if product.selected_or_first_available_variant.unit_price_measurement.reference_value != 1 -%} 78 | {{- product.selected_or_first_available_variant.unit_price_measurement.reference_value -}} 79 | {%- endif -%} 80 | {{ product.selected_or_first_available_variant.unit_price_measurement.reference_unit }} 81 | 82 |
83 |
84 |
85 | {%- if show_badges -%} 86 | 89 | 90 | 93 | {%- endif -%} 94 |
95 | -------------------------------------------------------------------------------- /shopify/snippets/product-card-placeholder.liquid: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | Renders a product card placeholder 3 | 4 | Usage: 5 | {% render 'product-card-placeholder' %} 6 | {% endcomment %} 7 | 8 |
9 | {{ 'onboarding.product_title' | t }} 10 | 11 |
12 |

{{ 'onboarding.product_title' | t }}

13 |
14 | 15 |
16 |
17 | {{ 'accessibility.vendor' | t }} 18 |
{{ 'products.product.vendor' | t }}
19 | {% render 'price' %} 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /shopify/snippets/social-sharing.liquid: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /shopify/templates/404.json: -------------------------------------------------------------------------------- 1 | { 2 | "sections": { 3 | "main": { 4 | "type": "main-404" 5 | } 6 | }, 7 | "order": [ 8 | "main" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /shopify/templates/article.json: -------------------------------------------------------------------------------- 1 | { 2 | "sections": { 3 | "main": { 4 | "type": "main-article", 5 | "blocks": { 6 | "featured_image": { 7 | "type": "featured_image" 8 | }, 9 | "title": { 10 | "type": "title" 11 | }, 12 | "content": { 13 | "type": "content" 14 | }, 15 | "social_sharing": { 16 | "type": "social_sharing" 17 | } 18 | }, 19 | "block_order": [ 20 | "featured_image", 21 | "title", 22 | "content", 23 | "social_sharing" 24 | ] 25 | } 26 | }, 27 | "order": ["main"] 28 | } 29 | -------------------------------------------------------------------------------- /shopify/templates/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "sections": { 3 | "main": { 4 | "type": "main-blog", 5 | "blocks": { 6 | "title": { 7 | "type": "title" 8 | }, 9 | "summary": { 10 | "type": "summary" 11 | }, 12 | "link": { 13 | "type": "link" 14 | } 15 | }, 16 | "block_order": [ 17 | "title", 18 | "summary", 19 | "link" 20 | ] 21 | } 22 | }, 23 | "order": [ 24 | "main" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /shopify/templates/cart.json: -------------------------------------------------------------------------------- 1 | { 2 | "sections": { 3 | "cart-items": { 4 | "type": "main-cart-items" 5 | }, 6 | "cart-footer": { 7 | "type": "main-cart-footer", 8 | "blocks": { 9 | "subtotal": { 10 | "type": "subtotal" 11 | }, 12 | "buttons": { 13 | "type": "buttons" 14 | } 15 | }, 16 | "block_order": [ 17 | "subtotal", 18 | "buttons" 19 | ] 20 | } 21 | }, 22 | "order": [ 23 | "cart-items", 24 | "cart-footer" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /shopify/templates/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "sections": { 3 | "banner": { 4 | "type": "main-collection-banner" 5 | }, 6 | "product-grid": { 7 | "type": "main-collection-product-grid" 8 | } 9 | }, 10 | "order": [ 11 | "banner", 12 | "product-grid" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /shopify/templates/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "sections": { 3 | "image_banner": { 4 | "type": "image-banner", 5 | "settings": { 6 | "desktop_text_box_position": "flex-end" 7 | }, 8 | "blocks": { 9 | "heading": { 10 | "type": "heading", 11 | "settings": { 12 | "heading": "Talk about your brand" 13 | } 14 | }, 15 | "button": { 16 | "type": "buttons", 17 | "settings": { 18 | "button_label_1": "Shop all", 19 | "button_link_1": "shopify://collections/all", 20 | "button_label_2": "" 21 | } 22 | } 23 | }, 24 | "block_order": ["heading", "button"] 25 | }, 26 | "featured_products": { 27 | "type": "featured-collection", 28 | "settings": { 29 | "title": "Featured products" 30 | } 31 | }, 32 | "image_text": { 33 | "type": "image-with-text", 34 | "blocks": { 35 | "heading": { 36 | "type": "heading" 37 | }, 38 | "text": { 39 | "type": "text" 40 | }, 41 | "button": { 42 | "type": "button" 43 | } 44 | }, 45 | "block_order": [ 46 | "heading", 47 | "text", 48 | "button" 49 | ] 50 | } 51 | }, 52 | "order": [ 53 | "image_banner", 54 | "featured_products", 55 | "image_text" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /shopify/templates/list-collections.json: -------------------------------------------------------------------------------- 1 | { 2 | "sections": { 3 | "main": { 4 | "type": "main-list-collections" 5 | } 6 | }, 7 | "order": [ 8 | "main" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /shopify/templates/page.contact.json: -------------------------------------------------------------------------------- 1 | { 2 | "sections": { 3 | "main": { 4 | "type": "main-page" 5 | }, 6 | "form": { 7 | "type": "contact-form" 8 | } 9 | }, 10 | "order": [ 11 | "main", 12 | "form" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /shopify/templates/page.json: -------------------------------------------------------------------------------- 1 | { 2 | "sections": { 3 | "main": { 4 | "type": "main-page" 5 | } 6 | }, 7 | "order": [ 8 | "main" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /shopify/templates/password.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": "password", 3 | "sections": { 4 | "main": { 5 | "type": "newsletter", 6 | "settings": { 7 | "full_width": false 8 | }, 9 | "blocks": { 10 | "heading": { 11 | "type": "heading", 12 | "settings": { 13 | "heading": "Opening soon" 14 | } 15 | }, 16 | "paragraph": { 17 | "type": "paragraph", 18 | "settings": { 19 | "paragraph": "

Be the first to know when we launch.

" 20 | } 21 | }, 22 | "email_form": { 23 | "type": "email_form" 24 | } 25 | }, 26 | "block_order": [ 27 | "heading", 28 | "paragraph", 29 | "email_form" 30 | ] 31 | } 32 | }, 33 | "order": [ 34 | "main" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /shopify/templates/product.json: -------------------------------------------------------------------------------- 1 | { 2 | "sections": { 3 | "main": { 4 | "type": "main-product", 5 | "blocks": { 6 | "vendor": { 7 | "type": "text", 8 | "settings": { 9 | "text_style": "uppercase", 10 | "text": "{{ product.vendor }}" 11 | } 12 | }, 13 | "title": { 14 | "type": "title" 15 | }, 16 | "subtitle": { 17 | "type": "text", 18 | "settings": { 19 | "text": "{{ product.metafields.descriptors.subtitle.value }}", 20 | "text_style": "subtitle" 21 | } 22 | }, 23 | "price": { 24 | "type": "price" 25 | }, 26 | "variant_picker": { 27 | "type": "variant_picker" 28 | }, 29 | "quantity_selector": { 30 | "type": "quantity_selector" 31 | }, 32 | "buy_buttons": { 33 | "type": "buy_buttons" 34 | }, 35 | "description": { 36 | "type": "description" 37 | }, 38 | "share": { 39 | "type": "share" 40 | } 41 | }, 42 | "block_order": [ 43 | "vendor", 44 | "title", 45 | "subtitle", 46 | "price", 47 | "variant_picker", 48 | "quantity_selector", 49 | "buy_buttons", 50 | "description", 51 | "share" 52 | ] 53 | }, 54 | "product-recommendations": { 55 | "type": "product-recommendations" 56 | } 57 | }, 58 | "order": [ 59 | "main", 60 | "product-recommendations" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /shopify/templates/search.json: -------------------------------------------------------------------------------- 1 | { 2 | "sections": { 3 | "main": { 4 | "type": "main-search" 5 | } 6 | }, 7 | "order": [ 8 | "main" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/__test__/cart/cart.spec.ts: -------------------------------------------------------------------------------- 1 | // import * as Cart from '../../helpers/cart/cart' 2 | 3 | // describe("Cart API", () => { 4 | // // test("it should filter by a search term (link)", () => { 5 | // // Cart.addItem() 6 | 7 | // // }); 8 | // }); 9 | -------------------------------------------------------------------------------- /src/__test__/dom/dom.spec.ts: -------------------------------------------------------------------------------- 1 | import { addClass, qs, qsa } from '../../helpers/dom/dom'; 2 | 3 | /** 4 | * @see https://noriste.github.io/reactjsday-2019-testing-course/book/intro-to-react-testing/jest-dom.html 5 | */ 6 | describe("DOM helpers test kmacoders", () => { 7 | test("Test qsa", () => { 8 | document.body.innerHTML = ` 9 |
10 |
kmacoders
11 |
12 |
13 | `; 14 | 15 | const output = 1; 16 | const dataTestValue = qsa('.xo-item')[0].getAttribute('data-test'); 17 | 18 | expect(qsa('.xo-item').length).toBe(1); 19 | expect(dataTestValue).toEqual('1'); 20 | expect(qsa('.xo-item2')[0]).toBeEmptyDOMElement; 21 | }); 22 | 23 | test("test qs", () => { 24 | document.body.innerHTML = ` 25 |
26 |
kmacoders
27 |
28 | `; 29 | 30 | const output = true; 31 | const vanillaDOM = document.querySelector('.xo-item'); 32 | expect(qs('.xo-item') === vanillaDOM).toBe(output); 33 | }); 34 | 35 | test("test addClass", () => { 36 | document.body.innerHTML = ` 37 |
38 |
kmacoders
39 |
40 | `; 41 | 42 | const output = true; 43 | const xoItem = document.querySelector('.xo-item') as HTMLElement; 44 | addClass(xoItem, 'new-class'); 45 | 46 | expect(xoItem.classList.contains('new-class')).toBe(output); 47 | }); 48 | test("Test qsa", () => { 49 | document.body.innerHTML = ` 50 |
51 |
kmacoders
52 |
53 |
54 | `; 55 | 56 | const output = 1; 57 | const dataTestValue = qsa('.xo-item')[0].getAttribute('data-test'); 58 | 59 | expect(qsa('.xo-item').length).toBe(1); 60 | expect(dataTestValue).toEqual('1'); 61 | expect(qsa('.xo-item2')[0]).toBeEmptyDOMElement; 62 | }); 63 | 64 | test("test qs", () => { 65 | document.body.innerHTML = ` 66 |
67 |
kmacoders
68 |
69 | `; 70 | 71 | const output = true; 72 | const vanillaDOM = document.querySelector('.xo-item'); 73 | expect(qs('.xo-item') === vanillaDOM).toBe(output); 74 | }); 75 | 76 | test("test addClass", () => { 77 | document.body.innerHTML = ` 78 |
79 |
kmacoders
80 |
81 | `; 82 | 83 | const output = true; 84 | const xoItem = document.querySelector('.xo-item') as HTMLElement; 85 | addClass(xoItem, 'new-class'); 86 | 87 | expect(xoItem.classList.contains('new-class')).toBe(output); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/__test__/test/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { filterByTerm } from '../../app' 2 | 3 | describe("Filter function", () => { 4 | test("it should filter by a search term (link)", () => { 5 | const input = [ 6 | { id: 1, url: "https://www.url1.dev" }, 7 | { id: 2, url: "https://www.url2.dev" }, 8 | { id: 3, url: "https://www.link3.dev" } 9 | ]; 10 | 11 | const output = [{ id: 3, url: "https://www.link3.dev" }]; 12 | 13 | expect(filterByTerm(input, "link")).toEqual(output); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/helpers/dom/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * 4 | * ``` 5 | * const styleEl = ` 6 | * 11 | * `; 12 | * 13 | * appendToHead({ 14 | * type: 'html', 15 | * childEl: styleEl, 16 | * }); 17 | * ``` 18 | * @param config 19 | */ 20 | const appendToHead = (config: { 21 | type: 'html' | 'element', 22 | childEl: string | HTMLStyleElement | HTMLScriptElement 23 | }): void => { 24 | const { type, childEl } = config; 25 | const headHTML = document.head || document.getElementsByTagName('head')[0]; 26 | 27 | type === 'html' && headHTML.insertAdjacentHTML('beforeend', childEl as string); 28 | type === 'element' && headHTML.insertAdjacentElement('beforeend', childEl as HTMLStyleElement | HTMLScriptElement); 29 | }; 30 | 31 | /** 32 | * 33 | * @example 34 | * ``` 35 | * const previewElement = document.querySelector('.preview-el'); 36 | * 37 | * css(previewElement, { 38 | * display: 'none', 39 | * color: '#f00'; 40 | * font-size: '2rem'; 41 | * }); 42 | * ``` 43 | * 44 | * @param el 45 | * @param styleObj 46 | */ 47 | const css = (el: HTMLElement, styleObj: any): void => { 48 | Object.keys(styleObj).forEach((key: any) => { 49 | // eslint-disable-next-line no-param-reassign 50 | el.style[key] = styleObj[key]; 51 | }); 52 | }; 53 | 54 | /** 55 | * Active only classname in ctx 56 | * @example 57 | * 58 | * ``` 59 | * const btn = document.getElementById('btn'); 60 | * const wrapper = document.querySelector('.xo-wrapper'); 61 | * 62 | * btn.addEventListener('click', () => { 63 | * activeClass(btn, 'xo--active', wrapper); 64 | * }) 65 | * ``` 66 | * 67 | * @param el - Target element 68 | * @param className - Class to toggle to target element 69 | * @param ctx - Context document 70 | */ 71 | const activeClassInCtx = (el: HTMLElement, className: string, ctx: T | Document = document): void => { 72 | ctx.querySelector(`.${className}`)!.classList.remove(className); 73 | el.classList.add(className); 74 | }; 75 | 76 | export { 77 | css, 78 | activeClassInCtx, 79 | appendToHead, 80 | }; 81 | -------------------------------------------------------------------------------- /src/helpers/dom/event.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Add new event to element 3 | * 4 | * #### Example 5 | * 6 | * ``` 7 | * const btn = byId('submit'); 8 | * const handler = (e) => { ... } 9 | * events.on(btn, 'click', handler); 10 | * ``` 11 | * 12 | */ 13 | const $on = (target: Document | T, type: string, handler: EventListener): void => { 14 | target.addEventListener(type, handler); 15 | }; 16 | 17 | /** 18 | * turn off event from element 19 | * #### Example 20 | * 21 | * ``` 22 | * const btn = byId('submit'); 23 | * const handler = (e) => { ... } 24 | * events.on(btn, 'click', handler); 25 | * events.off(btn, 'click', handler); 26 | * ``` 27 | * 28 | */ 29 | 30 | const $off = (target: Document, type: string, handler: EventListener): void => { 31 | target.removeEventListener(type, handler); 32 | }; 33 | 34 | export { 35 | $on, 36 | $off, 37 | }; 38 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | // Helper nào cần import ở index mới dùng được thì import ở đây 2 | // Helper dạng require lúc code trong các components thì thôi :v 3 | -------------------------------------------------------------------------------- /src/helpers/sections/index.ts: -------------------------------------------------------------------------------- 1 | import { qs } from '../dom/dom'; 2 | 3 | /** 4 | * Run script callback when a section is selected 5 | * @param {string} className: Root classname section 6 | * @param {Function} cb: Callback when this section is selected 7 | */ 8 | export const onSectionSelected = (className: string, cb: () => void): void => { 9 | qs(className) && cb(); 10 | }; 11 | 12 | /** 13 | * Debounce function 14 | * @param {Function} func : Function want to delay 15 | * @param {number} waitFor : Time to delay 16 | * @returns {Function} 17 | */ 18 | 19 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 20 | // eslint-disable-next-line @typescript-eslint/explicit-function-return-type 21 | export const debounce = any>(func: F, waitFor: number) => { 22 | let timeout = 0; 23 | 24 | const debounced = (...args: any): void => { 25 | clearTimeout(timeout); 26 | timeout = setTimeout(() => func(...args), waitFor); 27 | }; 28 | 29 | return debounced as (...args: Parameters) => ReturnType; 30 | }; 31 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-nocheck 3 | import Vue, { createApp } from 'vue'; 4 | import 'Vue/config'; 5 | import 'Vue/filters'; 6 | import store from './vue/store'; 7 | 8 | /** 9 | * SCSS 10 | */ 11 | import 'swiper/swiper-bundle.css'; 12 | import './styles/main.scss'; 13 | 14 | /** 15 | * TS 16 | */ 17 | import './helpers'; 18 | import './vue/components/entry'; 19 | 20 | /** 21 | * Auto find and import all .ts file in Shopify folder 22 | */ 23 | const tsFiles = require.context('Shopify/', true, /\.ts$/); 24 | tsFiles.keys().forEach(tsFiles); 25 | 26 | /** 27 | * vue components 28 | * auto-import all vue components 29 | */ 30 | const vueComponents = require.context('./vue/components/globals/', true, /\.vue$/); 31 | // vueComponents.keys().forEach((key) => { 32 | // const component = vueComponents(key).default; 33 | // Vue.component(component.name, component); 34 | // }); 35 | 36 | /** 37 | * All SECTION is vue instance ( template vue ) 38 | * 39 | * Properly render vue components inside sections on user insert in the theme editor 40 | * add the 'vue' keyword to the section's wrapper classes e.g.: 41 | * 42 | * {% schema %} 43 | * { 44 | * "class": "vue-section" 45 | * } 46 | * {% endschema %} 47 | */ 48 | 49 | /* If merchant in designMode */ 50 | Shopify.designMode && document.addEventListener('shopify:section:load', (event) => { 51 | if (event.target.classList.value.includes('vue-section')) { 52 | const app = createApp({ 53 | delimiters: ['${', '}'], 54 | store, 55 | }); 56 | 57 | vueComponents.keys().forEach((key) => { 58 | const component = vueComponents(key).default; 59 | app.component(component.name, component); 60 | }); 61 | 62 | app.mount(event.target); 63 | } 64 | }); 65 | 66 | /* If merchant in normalMode ( is Section ) */ 67 | document.querySelectorAll('.shopify-section').forEach((section) => { 68 | if (section.classList.value.includes('vue-section')) { 69 | const app = createApp({ 70 | delimiters: ['${', '}'], 71 | store, 72 | }); 73 | 74 | vueComponents.keys().forEach((key) => { 75 | const component = vueComponents(key).default; 76 | app.component(component.name, component); 77 | }); 78 | 79 | app.mount(section); 80 | } 81 | }); 82 | 83 | /** If vue instace != section */ 84 | document.querySelectorAll('[data-vue-instance]').forEach((element) => { 85 | const newInstanceVue = createApp({ 86 | el: element, 87 | store, 88 | }); 89 | vueComponents.keys().forEach((key) => { 90 | const component = vueComponents(key).default; 91 | newInstanceVue.component(component.name, component); 92 | }); 93 | }); 94 | 95 | console.log('kmacoders developing..'); 96 | -------------------------------------------------------------------------------- /src/styles/_general.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/_general.scss -------------------------------------------------------------------------------- /src/styles/_vueGeneral.scss: -------------------------------------------------------------------------------- 1 | [v-cloak] { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/base/_base-color.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/base/_base-color.scss -------------------------------------------------------------------------------- /src/styles/base/_base-dir.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/base/_base-dir.scss -------------------------------------------------------------------------------- /src/styles/base/_heading-base.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/base/_heading-base.scss -------------------------------------------------------------------------------- /src/styles/components/_components-dir.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/components/_components-dir.scss -------------------------------------------------------------------------------- /src/styles/components/button/_btn.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/components/button/_btn.scss -------------------------------------------------------------------------------- /src/styles/components/loading/_loading-ui.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/components/loading/_loading-ui.scss -------------------------------------------------------------------------------- /src/styles/components/toast/_toast.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/components/toast/_toast.scss -------------------------------------------------------------------------------- /src/styles/helpers/_helpers-dir.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/helpers/_helpers-dir.scss -------------------------------------------------------------------------------- /src/styles/layout/_layout-dir.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/layout/_layout-dir.scss -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | /* All import */ 2 | 3 | /* 4 | * Vendor 5 | */ 6 | @import './vendors/reset'; 7 | @import './vendors/variants'; // File chứa variable cho file grid bên dứoi 8 | @import './vendors/grid'; // Grid BS4 9 | @import './vendors/normalize'; // Reset CSS 10 | 11 | /* 12 | * Utils 13 | */ 14 | @import './utils/utils-dir'; 15 | 16 | /* 17 | * Pages 18 | */ 19 | @import './pages/pages-dir'; 20 | 21 | /* 22 | * Layout 23 | */ 24 | @import './layout/layout-dir'; 25 | 26 | /* 27 | * Components 28 | */ 29 | @import './components/components-dir'; 30 | 31 | /* 32 | * Snippet 33 | */ 34 | @import './snippet/background-image'; 35 | 36 | /* 37 | * Sections 38 | */ 39 | @import './sections/sections-dir'; 40 | 41 | /* 42 | * Base 43 | */ 44 | @import './base/base-dir'; 45 | 46 | /* 47 | * Helpers 48 | */ 49 | @import './helpers/helpers-dir'; 50 | 51 | /* Auto impo@import all scss file from Theme folder */ 52 | /* Using @see https://www.npmjs.com/package/impo@import-glob-loader */ 53 | @import "../../shopify/**/*.scss"; 54 | 55 | /* 56 | * Code chung 57 | */ 58 | @import './general'; 59 | @import './vueGeneral'; 60 | -------------------------------------------------------------------------------- /src/styles/pages/_body.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/pages/_body.scss -------------------------------------------------------------------------------- /src/styles/pages/_pages-dir.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/pages/_pages-dir.scss -------------------------------------------------------------------------------- /src/styles/pages/collection/_all-collection.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/pages/collection/_all-collection.scss -------------------------------------------------------------------------------- /src/styles/pages/collection/_normal-collection.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/pages/collection/_normal-collection.scss -------------------------------------------------------------------------------- /src/styles/pages/collection/_page-alles.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/pages/collection/_page-alles.scss -------------------------------------------------------------------------------- /src/styles/pages/customer/_account.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/pages/customer/_account.scss -------------------------------------------------------------------------------- /src/styles/sections/_sections-dir.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/styles/snippet/_background-image.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/styles/snippet/_background-image.scss -------------------------------------------------------------------------------- /src/styles/utils/_utils-dir.scss: -------------------------------------------------------------------------------- 1 | @import './mixins/prefix'; 2 | @import './mixins/center'; 3 | @import './mixins/position'; 4 | @import './mixins/responsive'; 5 | @import './mixins/size'; 6 | @import './functions/get-variable-css'; 7 | @import './mixins/three-dots'; 8 | -------------------------------------------------------------------------------- /src/styles/utils/functions/_get-variable-css.scss: -------------------------------------------------------------------------------- 1 | /// Get color variable from css to scss 2 | /// @param $color-props: color variable : --color-text-rgb, --color-body-text 3 | /// 4 | @function color($color-props) { 5 | @return var(--color-#{$color-props}); 6 | } 7 | 8 | /// Get font variable from css to scss 9 | /// @param $font-props: font variable : --font-size-header, --font-size-base 10 | /// 11 | @function font($font-props) { 12 | @return var(--font-#{$font-props}); 13 | } 14 | 15 | /// Get all general variable from css to scss 16 | @function v($props) { 17 | @return var(--#{$props}); 18 | } 19 | 20 | /// Example : 21 | // :root { 22 | // --color-background: #FFFFFF; 23 | // } 24 | 25 | // body { 26 | // color: color(primary); 27 | // } 28 | 29 | // compiled sass code is: 30 | 31 | // body { 32 | // color: var(--color-primary); 33 | // } 34 | 35 | // .kmacoders { 36 | // color: v(--color-background); 37 | // } 38 | 39 | // compiled sass code is: 40 | 41 | // .kmacoders { 42 | // color: var(----color-background); 43 | // } 44 | -------------------------------------------------------------------------------- /src/styles/utils/mixins/_center.scss: -------------------------------------------------------------------------------- 1 | /* Self center vertical */ 2 | @mixin vertical-align-center { 3 | position: relative; 4 | top: 50%; 5 | @include prefix(transform, translateY(-50%)); 6 | } 7 | 8 | /* Self center horizontal */ 9 | @mixin horizontal-align-center { 10 | position: relative; 11 | left: 50%; 12 | @include prefix(transform, translateX(-50%)); 13 | } 14 | 15 | /* Self center both direction */ 16 | @mixin both-align-center { 17 | position: relative; 18 | left: 50%; 19 | top: 50%; 20 | @include prefix(transform, translate(-50%, -50%)); 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/utils/mixins/_position.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Add position property 3 | * 4 | * @param {String} $position - relative | absolute | fixed 5 | * @param {Length} $args - List direction property and value 6 | * 7 | * @example 8 | * - Usage: 9 | * .foo { 10 | * @include position(relative, top 0 left 1em); 11 | * } 12 | * - Output: 13 | * .foo { 14 | position: relative; 15 | top: 0; 16 | left: 1em; 17 | * } 18 | * ...or other way : 19 | * .foo { 20 | * @include position(relative, ''); 21 | * } 22 | */ 23 | @mixin position($position, $args) { 24 | @each $o in top right bottom left { 25 | $i: index($args, $o); 26 | 27 | @if $i and $i + 1<= length($args) and type-of(nth($args, $i + 1)) == number { 28 | #{$o}: nth($args, $i + 1); 29 | } 30 | } 31 | 32 | position: $position; 33 | } 34 | -------------------------------------------------------------------------------- /src/styles/utils/mixins/_prefix.scss: -------------------------------------------------------------------------------- 1 | /* VERSION 1 ***********************************************/ 2 | /* 3 | * Add prefix for property css 4 | * 5 | * @param $name : name of property 6 | * @param $value : value of property 7 | * 8 | * @example 9 | * - Usage: 10 | * .foo { 11 | * @inlucde prefix(transform, translateY(-50%)); 12 | * } 13 | */ 14 | @mixin prefix($name, $value) { 15 | -webkit-#{$name}: $value; 16 | -moz-#{$name}: $value; 17 | -ms-#{$name}: $value; 18 | -o-#{$name}: $value; 19 | #{$name}: $value; 20 | } 21 | 22 | /* VERSION 2 : Avanced version ****************************************/ 23 | /* 24 | * Mixin to prefix several properties at once 25 | * 26 | * @param {List} $prefixes (()) - List of prefixes to print 27 | * @example 28 | * - Usage: 29 | * .foo { 30 | * @include prefix(( 31 | * column-count: 3, 32 | * column-gap: 1.5em, 33 | * column-rule: 2px solid hotpink 34 | * ), webkit moz); 35 | * } 36 | * - Output: 37 | * .foo { 38 | * -webkit-column-count: 3; 39 | * -moz-column-count: 3; 40 | * column-count: 3; 41 | * -webkit-column-gap: 1.5em; 42 | * -moz-column-gap: 1.5em; 43 | * column-gap: 1.5em; 44 | * -webkit-column-rule: 2px solid hotpink; 45 | * -moz-column-rule: 2px solid hotpink; 46 | * column-rule: 2px solid hotpink; 47 | *} 48 | */ 49 | @mixin prefixServeral($declarations, $prefixes: ()) { 50 | @each $property, $value in $declarations { 51 | @each $prefix in $prefixes { 52 | #{'-' + $prefix + '-' + $property}: $value; 53 | } 54 | 55 | // Output standard non-prefixed declaration 56 | #{$property}: $value; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/styles/utils/mixins/_responsive.scss: -------------------------------------------------------------------------------- 1 | /* Screen size ( Size này cùng với Theme Config global cho JS) */ 2 | $SM: 576px; 3 | $MD: 768px; 4 | $LG: 992px; 5 | $XL: 1200px; 6 | 7 | /* 8 | * Responsive element follow by screen size 9 | * 10 | * @param $screen-size: screen size 11 | * 12 | * @example 13 | * .foo { 14 | * font-size: 10px; 15 | * @include responsive(MD) { 16 | * font-size : 12px; 17 | * } 18 | * @include responsive(XL) { 19 | * font-size : 14px; 20 | * } 21 | */ 22 | @mixin responsive($screen-size) { 23 | @if $screen-size == SM { 24 | @media only screen and (min-width: $SM) { 25 | @content; 26 | } 27 | } @else if $screen-size == MD { 28 | @media only screen and (min-width: $MD) { 29 | @content; 30 | } 31 | } @else if $screen-size == LG { 32 | @media only screen and (min-width: $LG) { 33 | @content; 34 | } 35 | } @else if $screen-size == XL { 36 | @media only screen and (min-width: $XL) { 37 | @content; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/styles/utils/mixins/_size.scss: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Set size for a element 4 | * 5 | * @param $width: width property 6 | * @param $height: height property 7 | * 8 | * @example 9 | * .foo { 10 | * @include box(10px, 5px); 11 | * } 12 | */ 13 | @mixin box($width, $height) { 14 | height: $height; 15 | width: $width; 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/utils/mixins/_three-dots.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Set size for a element 3 | * 4 | * @param $width: width property 5 | * @param $line: line property 6 | * 7 | * @example 8 | * .foo { 9 | * @include three-dots(200px, 3); 10 | * } 11 | */ 12 | 13 | @mixin three-dots($width, $line) { 14 | overflow: hidden; 15 | text-overflow: ellipsis; 16 | display: -webkit-box; 17 | -webkit-box-orient: vertical; 18 | -webkit-line-clamp: $line; 19 | 20 | width: $width; 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/vendors/_variants.scss: -------------------------------------------------------------------------------- 1 | $width-site: 1200px; 2 | $gutter-site: 15px; 3 | $gutter-site-mobile: 15px; 4 | $section-padding-top: 80px; 5 | $section-padding-bottom: 80px; 6 | $section-spacing-top: 10px; 7 | $section-spacing-bottom: 10px; 8 | $section-spacing-sm-top: 20px; 9 | $section-spacing-sm-bottom: 20px; 10 | $section-spacing-md-top: 30px; 11 | $section-spacing-md-bottom: 30px; 12 | $section-spacing-lg-top: 40px; 13 | $section-spacing-lg-bottom: 40px; 14 | $section-spacing-xl-top: 50px; 15 | $section-spacing-xl-bottom: 50px; 16 | $swiper-color: #007aff; 17 | -------------------------------------------------------------------------------- /src/types/shopify/cart.type.ts: -------------------------------------------------------------------------------- 1 | import { ILineItem } from './common.type'; 2 | 3 | export interface ICart { 4 | token?: string; 5 | note?: string; 6 | attributes?: any; 7 | original_total_price?: number; 8 | total_price?: number; 9 | total_discount?: number; 10 | total_weight?: number; 11 | item_count?: number; 12 | requires_shipping?: boolean; 13 | cart_level_discount_applications: any[]; 14 | currency: string; 15 | items: ILineItem[]; 16 | items_subtotal_price: number; 17 | } 18 | 19 | /** 20 | * Properties in formData request '/cart/add.js' 21 | */ 22 | export interface IProperty { 23 | [propsKey: string]: string|number; 24 | } 25 | 26 | export interface IItemsResponse { 27 | items: ILineItem[]; 28 | } 29 | -------------------------------------------------------------------------------- /src/types/shopify/collection.type.ts: -------------------------------------------------------------------------------- 1 | export interface ICollection { 2 | id: number; 3 | title: string; 4 | handle: string; 5 | description: string; 6 | pushlished_at: string; 7 | update_at: string; 8 | image: string; 9 | products_count: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/shopify/common.type.ts: -------------------------------------------------------------------------------- 1 | export interface ILineItem { 2 | id?: number; 3 | properties?: any; 4 | quantity?: number; 5 | variant_id?: number; 6 | key?: string; 7 | title?: string; 8 | price?: number; 9 | line_price?: number; 10 | original_line_price?: number; 11 | total_discount?: number; 12 | discounts?: any[]; 13 | discounted_price?: number; 14 | featured_image?: { 15 | alt: string, 16 | aspect_ratio: number; 17 | height: number; 18 | url: string; 19 | width: number; 20 | }; 21 | sku?: any; 22 | grams?: number; 23 | vendor?: string; 24 | product_id?: number; 25 | gift_card?: boolean; 26 | url?: string; 27 | image?: string; 28 | handle?: string; 29 | requires_shipping?: boolean; 30 | product_type?: string; 31 | product_title?: string; 32 | product_description?: string; 33 | variant_title?: string; 34 | variant_options?: string[]; 35 | final_line_price?: number; 36 | line_level_discount_allocations?: any[]; 37 | line_level_total_discount?: number; 38 | options_with_values?: { 39 | name: string; 40 | value: number; 41 | }[]; 42 | original_price?: number; 43 | product_has_only_default_variant?: boolean; 44 | taxable: boolean; 45 | } 46 | 47 | export interface IVariant { 48 | price: number; 49 | id: number; 50 | title?: string; 51 | option1?: string; 52 | option2?: any; 53 | option3?: any; 54 | sku?: any; 55 | requires_shipping?: boolean; 56 | taxable?: boolean; 57 | featured_image?: any; 58 | available?: boolean; 59 | name?: string; 60 | options?: string[]; 61 | weight?: number; 62 | compare_at_price?: any; 63 | inventory_quantity?: number; 64 | inventory_management?: string; 65 | inventory_policy?: string; 66 | barcode?: string; 67 | } 68 | 69 | export interface IOption { 70 | name?: string; 71 | position?: number; 72 | values?: string[]; 73 | } 74 | -------------------------------------------------------------------------------- /src/types/shopify/product.type.ts: -------------------------------------------------------------------------------- 1 | import { IOption, IVariant } from './common.type'; 2 | 3 | export interface IProduct { 4 | id: number; 5 | title: string; 6 | handle: string; 7 | variants: IVariant[]; 8 | description?: string; 9 | published_at: Date; 10 | created_at: Date; 11 | vendor?: string; 12 | type?: string; 13 | tags?: any[]; 14 | price: number; 15 | price_min?: number; 16 | price_max?: number; 17 | available?: boolean; 18 | price_varies?: boolean; 19 | compare_at_price?: any; 20 | compare_at_price_min?: number; 21 | compare_at_price_max?: number; 22 | compare_at_price_varies?: boolean; 23 | images?: string[]; 24 | featured_image?: string; 25 | options?: IOption[]; 26 | url?: string; 27 | media?: IMedia[]; 28 | } 29 | 30 | export interface IProductSearch { 31 | available: boolean; 32 | body: string; 33 | compare_at_price_max: string; 34 | compare_at_price_min: string; 35 | featured_image: { 36 | alt: string; 37 | aspect_ratio: number; 38 | height: number; 39 | url: string; 40 | width: number; 41 | }; 42 | handle: string; 43 | id: number; 44 | image: string; 45 | price: string; 46 | price_max: string; 47 | price_min: string; 48 | tags: string[]; 49 | title: string; 50 | type: string; 51 | url: string; 52 | variants: IVariant[]; 53 | vendor: string; 54 | } 55 | 56 | export interface IMedia { 57 | alt: string; 58 | aspect_ratio: number; 59 | height: number; 60 | id: number; 61 | media_type: 'video' | 'image' | 'external_video'| 'model'; 62 | position: number; 63 | preview_image: IPreviewObject; 64 | src: string; 65 | width: number; 66 | } 67 | 68 | export interface IPreviewObject { 69 | aspect_ratio: number; 70 | height: number; 71 | src: string; 72 | width: number; 73 | } 74 | -------------------------------------------------------------------------------- /src/types/shopify/theme.type.ts: -------------------------------------------------------------------------------- 1 | export interface ITheme { 2 | id: number; 3 | name: string; 4 | role: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/types/vue-shims.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Description: 3 | * Import Vue component to .ts file in ./Shopify folder 4 | */ 5 | 6 | declare module "*.vue" { 7 | import Vue from 'vue'; 8 | export default Vue; 9 | } 10 | -------------------------------------------------------------------------------- /src/vue/components/entry/cart/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/vue/components/entry/cart/.gitkeep -------------------------------------------------------------------------------- /src/vue/components/entry/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Import all entry 3 | * Mount Vue SFC vào DOM 4 | */ 5 | 6 | // import { onSectionSelected } from 'Helpers/sections'; 7 | // import SearchBar from './search/SearchBar.vue'; 8 | // import PageAlles from './alles/PageAlles.vue'; 9 | // import CartDrawer from './cart/CartDrawer.vue'; 10 | // import CartItemCount from '../globals/CartItemCount.vue'; 11 | // import ProductReview from './product-review/ProductReview.vue'; 12 | // import './alles/AllesContainer'; 13 | 14 | // onSectionSelected('#search-bar', () => { 15 | // const seachBar = new SearchBar().$mount('#search-bar'); 16 | // }); 17 | 18 | // onSectionSelected('#page-alles', () => { 19 | // const pageAlles = new PageAlles().$mount('#page-alles'); 20 | // }); 21 | 22 | // onSectionSelected('#product-review', () => { 23 | // const productReview = new ProductReview().$mount('#product-review'); 24 | // }); 25 | 26 | // const cartDrawer = new CartDrawer().$mount('#cart-drawer'); 27 | 28 | // onSectionSelected('#cart-item-count', () => { 29 | // const cartItemCount = new CartItemCount().$mount('#cart-item-count'); 30 | // }); 31 | -------------------------------------------------------------------------------- /src/vue/components/entry/product/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/vue/components/entry/product/.gitkeep -------------------------------------------------------------------------------- /src/vue/components/entry/readme.md: -------------------------------------------------------------------------------- 1 | Chứa các Component Vue của từng Page 2 | -------------------------------------------------------------------------------- /src/vue/components/entry/search/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/vue/components/entry/search/.gitkeep -------------------------------------------------------------------------------- /src/vue/components/globals/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmacoders/Shopify-Theme-Starter-Vue3/0bdc5d64a3a1a62c7f62a333c52ee066c7e2ad19/src/vue/components/globals/.gitkeep -------------------------------------------------------------------------------- /src/vue/components/globals/XoButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | 21 | 32 | -------------------------------------------------------------------------------- /src/vue/components/globals/readme.md: -------------------------------------------------------------------------------- 1 | Chứa các Components Vue được sử dụng ở nhiều page 2 | -------------------------------------------------------------------------------- /src/vue/config/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config vue global 3 | */ 4 | 5 | // import Vue from 'vue'; 6 | 7 | // Vue.config.ignoredElements = ['model-viewer']; 8 | -------------------------------------------------------------------------------- /src/vue/filters/hugMoneyFormat.ts: -------------------------------------------------------------------------------- 1 | import { formatMoney } from '@shopify/theme-currency'; 2 | 3 | declare let theme: any; 4 | 5 | /** 6 | * Format money from Shopify theo money_format 7 | */ 8 | 9 | const hugMoneyFormat = (value: number | string): string => { 10 | if (!value) return ''; 11 | return formatMoney(Number(value), theme.moneyFormat); 12 | }; 13 | 14 | export default hugMoneyFormat; 15 | -------------------------------------------------------------------------------- /src/vue/filters/hugUppercase.ts: -------------------------------------------------------------------------------- 1 | const hugUppercase = (value: string): string => value.toUpperCase(); 2 | 3 | export default hugUppercase; 4 | -------------------------------------------------------------------------------- /src/vue/filters/imgURL.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Same as img_url shopify theme 3 | */ 4 | 5 | const imgURL = (src: string, size: string, crop: string): string => src 6 | .replace(/_(pico|icon|thumb|small|compact|medium|large|grande|original|500x500|768x768|1024x1024|2048x2048|master)+\./g, '.') 7 | .replace(/\.jpg|\.png|\.gif|\.jpeg/g, (match: string) => `_${size}_crop_${crop}${match}`); 8 | 9 | export default imgURL; 10 | -------------------------------------------------------------------------------- /src/vue/filters/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Import Vue Filter global 3 | */ 4 | // import Vue from 'vue'; 5 | // import hugMonedyFormat from './hugMoneyFormat'; 6 | // import imgURL from './imgURL'; 7 | // import hugUppercase from './hugUppercase'; 8 | 9 | // Vue.filter('hugMoneyFormat', hugMonedyFormat); 10 | // Vue.filter('imgURL', imgURL); 11 | // Vue.filter('hugUppercase', hugUppercase); 12 | -------------------------------------------------------------------------------- /src/vue/mixins/CollectionCard.ts: -------------------------------------------------------------------------------- 1 | // import Vue from 'vue'; 2 | // import Component from 'vue-class-component'; 3 | // import store from 'Vue/store/index'; 4 | // import { updateTime } from 'Helpers/utils'; 5 | // import { IProduct } from 'Types/shopify/product.type'; 6 | // import { mapActions, mapGetters } from 'vuex'; 7 | // /** 8 | // * Description: 9 | // * Mixins Vue create list products card from 1 collection ( with collection id) 10 | // */ 11 | // declare let collectionId: string; 12 | 13 | // const getProductsFromCollection = async (): Promise => { 14 | // if (collectionId === 'NO_CHOOSEN_COLLECTION') return []; 15 | 16 | // const res = await fetch(`https://cdn.xopify.com/custom-app/upfrontreep/collections-${collectionId}.json?${updateTime()}`); 17 | // if (!res.ok) throw new Error('Bad response from server'); 18 | // const listProducts: IProduct[] = await res.json(); 19 | 20 | // return listProducts; 21 | // }; 22 | 23 | // @Component({ 24 | // store, 25 | // computed: { 26 | // ...mapGetters('CollectionStore', { 27 | // isCollectionLoading: 'getIsCollectionLoading', 28 | // }), 29 | // }, 30 | // methods: { 31 | // ...mapActions('CollectionStore', [ 32 | // 'onDisabledLoading', 33 | // ]), 34 | // }, 35 | // }) 36 | // export default class MixinCollectionCard extends Vue { 37 | // isCollectionLoading!: boolean; 38 | 39 | // onDisabledLoading!: () => void; 40 | 41 | // allProducts: IProduct[] = []; 42 | 43 | // async mounted(): Promise { 44 | // const products = await getProductsFromCollection(); 45 | // products.forEach((product) => { 46 | // this.allProducts.push(product); 47 | // }); 48 | 49 | // this.onDisabledLoading(); 50 | // } 51 | // } 52 | -------------------------------------------------------------------------------- /src/vue/mixins/SplitingVariant.ts: -------------------------------------------------------------------------------- 1 | // import { VariantsEntity } from 'Types/product-information/information'; 2 | // import Vue from 'vue'; 3 | // import Component from 'vue-class-component'; 4 | 5 | // /** 6 | // * Spliting variant title || count package variant 7 | // */ 8 | // @Component 9 | // export default class SplitingVariant extends Vue { 10 | // hoverVariant: VariantsEntity = {} as VariantsEntity; 11 | 12 | // get metaVariant(): string { 13 | // const res = (this.hoverVariant.title || '').split(' '); 14 | // let metaVariant = ''; 15 | // if (res.length > 1) { 16 | // (res.slice(0, -1)).forEach((title: string) => { 17 | // metaVariant += `${title} `; 18 | // }); 19 | // } else { 20 | // [metaVariant] = res; 21 | // } 22 | // return metaVariant || ''; 23 | // } 24 | 25 | // get quantityVariant(): string { 26 | // const res = (this.hoverVariant.title || '').split(' '); 27 | // const quantityVariant = (res.length > 1) ? res[Number(res.length - 1)] : ''; 28 | // return quantityVariant || ''; 29 | // } 30 | // } 31 | -------------------------------------------------------------------------------- /src/vue/store/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * All store 3 | */ 4 | // import Vue from 'vue'; 5 | // import Vuex from 'vuex'; 6 | // import CartStore from './modules/cart'; 7 | // import CollectionStore from './modules/collection'; 8 | // import ReviewStore from './modules/productReview'; 9 | // import SearchStore from './modules/header'; 10 | 11 | // Vue.use(Vuex); 12 | // const store = new Vuex.Store({ 13 | // modules: { 14 | // CartStore, 15 | // CollectionStore, 16 | // ReviewStore, 17 | // SearchStore, 18 | // }, 19 | // }); 20 | 21 | // export default store; 22 | -------------------------------------------------------------------------------- /src/vue/store/modules/cart/index.ts: -------------------------------------------------------------------------------- 1 | // import { getCartState, updateItemByKey } from 'Helpers/cart/cart'; 2 | // import { ICart, IProperty } from 'Types/shopify/cart.type'; 3 | // import { 4 | // Action, 5 | // Module, 6 | // Mutation, 7 | // VuexModule, 8 | // } from 'vuex-module-decorators'; 9 | 10 | // enum MutationType { 11 | // setCartState = 'SET_CART_STATE', 12 | // setIsExpand = 'SET_IS_EXPAND', 13 | // setCartItemCount = 'SET_CART_ITEM_COUNT', 14 | // } 15 | 16 | // declare let cartInitial: ICart; 17 | // declare let cartItemCountInitial: number; 18 | // @Module({ 19 | // namespaced: true, 20 | // }) 21 | // export default class CartStore extends VuexModule { 22 | // /** 23 | // * Toggle cart drawer 24 | // */ 25 | // isExpand = false; 26 | 27 | // /** 28 | // * Initital cart state ( page reload ) 29 | // */ 30 | // cartState: ICart = cartInitial; 31 | 32 | // /** 33 | // * Exist item in cart 34 | // */ 35 | // cartItemCount = cartItemCountInitial; 36 | 37 | // get getCartState(): ICart { 38 | // return this.cartState; 39 | // } 40 | 41 | // get getIsExpand(): boolean { 42 | // return this.isExpand; 43 | // } 44 | 45 | // get getCartItemCount(): number { 46 | // return this.cartItemCount; 47 | // } 48 | 49 | // @Mutation 50 | // [MutationType.setCartState](newCartState: ICart): void { 51 | // this.cartState = newCartState; 52 | // } 53 | 54 | // @Mutation 55 | // [MutationType.setCartItemCount](newCount: number): void { 56 | // this.cartItemCount = newCount || Number(this.cartState.item_count); 57 | // } 58 | 59 | // @Mutation 60 | // [MutationType.setIsExpand](): void { 61 | // this.isExpand = !this.isExpand; 62 | // } 63 | 64 | // @Action 65 | // async fetchCartState(): Promise { 66 | // const newCartState = await getCartState(); 67 | // this.context.commit(MutationType.setCartState, newCartState); 68 | // this.context.commit(MutationType.setCartItemCount, Number(newCartState.item_count)); 69 | // } 70 | 71 | // @Action 72 | // async updateItemByKey(config: { 73 | // key: string, 74 | // quantity?: number, 75 | // properties: IProperty, 76 | // onSuccess?: (cartState: ICart) => void; 77 | // onError?: (err: Error) => void; 78 | // }): Promise { 79 | // const { 80 | // key, 81 | // quantity, 82 | // properties, 83 | // onSuccess, 84 | // onError, 85 | // } = config; 86 | 87 | // const formData = { 88 | // id: key, 89 | // quantity, 90 | // properties, 91 | // }; 92 | 93 | // try { 94 | // const res = await fetch('/cart/change.js', { 95 | // method: 'POST', 96 | // headers: { 97 | // 'Content-Type': 'application/json', 98 | // }, 99 | // body: JSON.stringify(formData), 100 | // }); 101 | // if (!res.ok) throw new Error('Bad response from server'); 102 | // const cart: ICart = await res.json(); 103 | // this.context.commit(MutationType.setCartState, cart); 104 | // this.context.commit(MutationType.setCartItemCount, Number(cart.item_count)); 105 | // onSuccess && onSuccess(cart); 106 | // } catch (error) { 107 | // onError && onError(error); 108 | // } 109 | // } 110 | 111 | // @Action 112 | // toggleExpand(): void { 113 | // this.context.commit(MutationType.setIsExpand); 114 | // } 115 | // } 116 | -------------------------------------------------------------------------------- /src/vue/store/modules/collection/index.ts: -------------------------------------------------------------------------------- 1 | // import { 2 | // Action, 3 | // Module, 4 | // Mutation, 5 | // VuexModule, 6 | // } from 'vuex-module-decorators'; 7 | 8 | // enum MutationType { 9 | // setIsCollectionLoading = 'SET_IS_COLLECTION_LOADING', 10 | // } 11 | 12 | // @Module({ 13 | // namespaced: true, 14 | // }) 15 | // export default class CollectionStore extends VuexModule { 16 | // /** 17 | // * State of loading ui in Alles page 18 | // */ 19 | // isCollectionLoading = true; 20 | 21 | // get getIsCollectionLoading(): boolean { 22 | // return this.isCollectionLoading; 23 | // } 24 | 25 | // @Mutation 26 | // [MutationType.setIsCollectionLoading](): void { 27 | // this.isCollectionLoading = false; 28 | // } 29 | 30 | // @Action 31 | // onDisabledLoading(): void { 32 | // this.context.commit(MutationType.setIsCollectionLoading); 33 | // } 34 | // } 35 | -------------------------------------------------------------------------------- /src/vue/store/type.ts: -------------------------------------------------------------------------------- 1 | // export interface RootState { 2 | // cartCount: number, 3 | // isOpen: boolean, 4 | // } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "lib": [ 5 | "es6", 6 | "dom" 7 | ], 8 | "outDir": "./dist", 9 | // "rootDir": "./src", 10 | "allowJs": true, 11 | "module": "commonjs", 12 | "target": "es5", 13 | "strictPropertyInitialization": false, 14 | "sourceMap": false, 15 | "moduleResolution": "node", 16 | "strict": true, 17 | "experimentalDecorators": true, 18 | "types": ["google.maps"], 19 | 20 | /* Config alias for ts */ 21 | "baseUrl": ".", 22 | "paths": { 23 | "Shopify/*": [ "./shopify/*" ], // Shopify structure 24 | "Components/*": [ "./src/components/*" ], 25 | "Helpers/*": [ "./src/helpers/*" ], 26 | "Styles/*": [ "./src/styles/*" ], 27 | "Types/*": [ "./src/types/*" ], 28 | "Vue/*": [ "src/vue/*" ], 29 | }, 30 | }, 31 | "exclude": [ 32 | "node_modules", 33 | "**/*.spec.ts", 34 | "**/*.test.ts" 35 | ], 36 | "include": [ 37 | "./src/**/*.ts", 38 | "./src/**/*.vue", 39 | "./shopify/**/*.ts", 40 | "./shopify/**/*.vue", 41 | "./jest.setup.ts" 42 | ], 43 | } 44 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/helpers/", "src/types"], 3 | "out": "docs" 4 | } 5 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const { hugCommonConfig } = require('./build-utils/webpack.common'); 3 | 4 | const addons = (addonsArg) => { 5 | console.log(addonsArg); 6 | const addons2 = [] 7 | .concat.apply([], [addonsArg]) 8 | .filter(Boolean); 9 | 10 | return addons2.map((addonName) => require(`./build-utils/addons/webpack.${addonName}.js`)); 11 | }; 12 | 13 | const allConfigs = (env) => { 14 | console.log(env) 15 | console.log(env.addons) 16 | 17 | const envConfig = require(`./build-utils/webpack.${env.env}.js`); 18 | const allConfig = merge(hugCommonConfig, envConfig, ...addons(env.addons)); 19 | 20 | return allConfig; 21 | } 22 | module.exports = allConfigs; 23 | --------------------------------------------------------------------------------