├── .DS_Store
├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── config.yml.example
├── package.json
├── postcss.config.js
├── prettier.config.js
├── readme.md
├── shopify-dev-utils
├── convertToGlobalDataStructure.js
├── filters
│ ├── asset_url.js
│ ├── default_pagination.js
│ ├── money_with_currency.js
│ ├── money_without_trailing_zeros.js
│ ├── script_tag.js
│ ├── stylesheet_tag.js
│ └── within.js
├── liquidDev.entry.js
├── liquidDev.loader.js
├── section-tags
│ ├── index.d.ts
│ ├── index.js
│ ├── javascript.d.ts
│ ├── javascript.js
│ ├── schema.d.ts
│ ├── schema.js
│ ├── section.d.ts
│ ├── section.js
│ ├── stylesheet.d.ts
│ └── stylesheet.js
├── storeData.js
├── storefrontApi.js
├── tags
│ ├── paginate.d.ts
│ └── paginate.js
└── transformLiquid.js
├── src
├── assets
│ ├── favicon
│ │ └── .gitkeep
│ ├── fonts
│ │ ├── .gitkeep
│ │ ├── noto-serif
│ │ │ ├── NotoSerif-Bold.ttf
│ │ │ └── NotoSerif-Regular.ttf
│ │ └── poppins
│ │ │ ├── Poppins-ExtraBold.ttf
│ │ │ ├── Poppins-Light.ttf
│ │ │ ├── Poppins-Regular.ttf
│ │ │ ├── Poppins-SemiBold.ttf
│ │ │ └── Poppins-Thin.ttf
│ ├── images
│ │ └── great-success.png
│ └── svg
│ │ └── webpack.svg
├── components
│ ├── helpers
│ │ └── cart-fetch-api
│ │ │ └── cart-fetch-api.js
│ ├── layout
│ │ ├── theme.js
│ │ └── theme.liquid
│ ├── sections
│ │ ├── banner-with-text
│ │ │ └── banner-with-text.liquid
│ │ ├── cart
│ │ │ └── cart.liquid
│ │ ├── feature-collection
│ │ │ └── featured-collection.liquid
│ │ ├── featured-product
│ │ │ └── featured-product.liquid
│ │ ├── footer
│ │ │ └── footer.liquid
│ │ ├── header
│ │ │ └── header.liquid
│ │ ├── image-with-text
│ │ │ └── image-with-text.liquid
│ │ └── product
│ │ │ ├── product.js
│ │ │ └── product.liquid
│ ├── snippets
│ │ ├── dynamic-modal
│ │ │ └── dynamic-modal.liquid
│ │ ├── global-css
│ │ │ ├── global-css.js
│ │ │ ├── global-css.liquid
│ │ │ └── global-css.scss
│ │ ├── input-counter
│ │ │ ├── input-counter.js
│ │ │ ├── input-counter.liquid
│ │ │ └── input-counter.scss
│ │ ├── message
│ │ │ ├── message.js
│ │ │ ├── message.liquid
│ │ │ └── message.scss
│ │ ├── pagination
│ │ │ ├── accessible-pagination.liquid
│ │ │ └── default-pagination.liquid
│ │ ├── responsive-bg-image
│ │ │ └── responsive-bg-image.liquid
│ │ └── responsive-image
│ │ │ └── responsive-image.liquid
│ ├── tailwind.css
│ └── templates
│ │ ├── 404.liquid
│ │ ├── article.liquid
│ │ ├── blog.liquid
│ │ ├── cart.liquid
│ │ ├── collection.liquid
│ │ ├── customers
│ │ ├── account.liquid
│ │ ├── activate_account.liquid
│ │ ├── addresses.liquid
│ │ ├── login.liquid
│ │ ├── order.liquid
│ │ ├── register.liquid
│ │ └── reset_password.liquid
│ │ ├── gift_card.liquid
│ │ ├── index.liquid
│ │ ├── list-collections.liquid
│ │ ├── page.contact.liquid
│ │ ├── page.liquid
│ │ ├── product.liquid
│ │ └── search.liquid
├── config
│ ├── settings_data.json
│ └── settings_schema.json
└── locales
│ └── en.default.json
├── tailwind.config.js
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3daddict/themekit-webpack/5f05ecbb27a7493d4bb01cb24035fa1b4145208f/.DS_Store
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"],
3 | "plugins": ["@babel/plugin-transform-runtime"]
4 | }
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /dist/**
2 | /shopify-dev-utils/**
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["plugin:@shopify/esnext", "plugin:@shopify/node", "prettier"],
4 | "rules": {
5 | "quotes": [2, "single", { "avoidEscape": true }],
6 | "no-unused-vars": "warn",
7 | "no-undef": "off"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | *.DS_Store
3 | dist
4 | node_modules
5 | *.lock
6 | package-lock.json
7 | *.yml
8 | *error.log
9 | .idea
10 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at themekit@3daddict.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing
2 |
3 | First off, thank you for considering contributing to A Shopify Theme with Themekit and Webpack. It's exciting to see what we can do with modern build tools.
4 |
5 | ### Where do I go from here?
6 |
7 | If you notice a bug or have a feature request, [make one](https://github.com/activeadmin/activeadmin/issues/new)! I'm no pro, creating issues helps improve this repo and helps learn about all that is involved building themes.
8 | Please make sure to check the [CODE_OF_CONDUCT.md](https://github.com/3daddict/themekit-webpack/blob/master/CODE_OF_CONDUCT.md) before contributing to this repo.
9 |
10 | ### Fork & create a branch
11 |
12 | If there is something you think you can fix, then [fork themekit-webpack](https://help.github.com/articles/fork-a-repo) and create
13 | a branch with a descriptive name.
14 |
15 | A good branch name would be (where issue #33 is the ticket you're working on):
16 |
17 | ```sh
18 | git checkout -b 33-add-infinite-scroll-feature
19 | ```
20 |
21 | ### Verify build with ESLint
22 | When testing your new feature ensure you are passing ESLint rules by running
23 | ```sh
24 | yarn build
25 | ```
26 | Make note of any errors that get flagged from ESLint and fix before creating a PR.
27 |
28 | ### Make a Pull Request
29 |
30 | At this point, you should switch back to your master branch and make sure it's
31 | up to date with themekit-webpack's master branch:
32 |
33 | ```sh
34 | git remote add upstream git@github.com:themekit-webpack/themekit-webpack.git
35 | git checkout master
36 | git pull upstream master
37 | ```
38 |
39 | Then update your feature branch from your local copy of master, and push it!
40 |
41 | ```sh
42 | git checkout 33-add-infinite-scroll-feature
43 | git rebase master
44 | git push --set-upstream origin 33-add-infinite-scroll-feature
45 | ```
46 | Finally, go to GitHub and [make a Pull Request](https://help.github.com/articles/creating-a-pull-request) 😎
47 | ### Keeping your Pull Request updated
48 |
49 | If a maintainer asks you to "rebase" your PR, they're saying that a lot of code
50 | has changed, and that you need to update your branch so it's easier to merge.
51 |
52 | To learn more about rebasing in Git, there are a lot of good [git rebasing](http://git-scm.com/book/en/Git-Branching-Rebasing) [resources](https://help.github.com/en/github/using-git/about-git-rebase) but here's the suggested workflow:
53 |
54 | ```sh
55 | git checkout 33-add-infinite-scroll-feature
56 | git pull --rebase upstream master
57 | git push --force-with-lease 33-add-infinite-scroll-feature
58 | ```
59 | ### Merging a PR (maintainers only)
60 |
61 | A PR can only be merged into `master` by a maintainer if:
62 |
63 | * It is passing ESLint.
64 | * It has no requested changes.
65 | * It is up to date with current master.
66 |
67 | Any maintainer is allowed to merge a PR if all of these conditions are met.
68 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 3daddict
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/config.yml.example:
--------------------------------------------------------------------------------
1 | # Copy or rename the config.yml.example to config.yml
2 | # You can find your shopify credentials by going to your Shopify admin and creating a Private App
3 | # See more info below on how to create a Private app and cloned themes
4 |
5 | # Credentials for yarn start
6 | development:
7 | password:
8 | theme_id: ""
9 | store:
10 | storefront_api_key:
11 | directory: dist/
12 | # Note: when first deploying to a new theme you may have to comment out the ignore files.
13 | # Then after first upload uncomment them.
14 | ignore_files:
15 | - config/settings_data.json
16 | - "*.js"
17 | - "*.hot-update.json"
18 |
19 | # Credentials for yarn build & deploy
20 | production:
21 | password:
22 | theme_id: ""
23 | store:
24 | directory: dist/
25 | ignore_files:
26 | - config/settings_data.json
27 |
28 | ########### Creating a Private App ###########
29 | # Click Apps from the admin sidebar
30 | # Scroll down to the bottom and click on Manage private apps
31 | # Create a new private app by clicking the button
32 | # Name your app and add an email
33 | # Under the Admin API expand the selection and scroll down to Themes, change to Read and write
34 | # Under the Storefront API check the checkbox: Allow this app to access your storefront data using the Storefront API
35 | # Click the save button and grab the credentials to add below for the yml config
36 |
37 | ########### Cloning a Theme ###########
38 | # It's not recommended to work off your published theme
39 | # Duplicate a theme by going to Actions on your published theme and clicking Duplicate
40 | # Rename your theme and then click edit code
41 | # Look at the url and grab the theme id at the end of it for example: 116723349937
42 | # This is the theme id to add above in the yml config
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "themekit-webpack",
3 | "version": "1.0.0",
4 | "main": "webpack.config.js",
5 | "license": "MIT",
6 | "keywords": [],
7 | "author": "Mike Salvati",
8 | "private": true,
9 | "scripts": {
10 | "start": "TAILWIND_MODE=watch webpack serve --mode=development --config webpack.dev.js",
11 | "build": "webpack --mode=production --config webpack.prod.js && npx tailwindcss build -i src/components/tailwind.css -o dist/assets/tailwind.css.liquid && cleancss -o dist/assets/tailwind.min.css.liquid dist/assets/tailwind.css.liquid",
12 | "deploy": "webpack --mode=production --config webpack.prod.js && npx tailwindcss build -i src/components/tailwind.css -o dist/assets/tailwind.css.liquid && cleancss -o dist/assets/tailwind.min.css.liquid dist/assets/tailwind.css.liquid && shopify-themekit deploy --env=production"
13 | },
14 | "dependencies": {
15 | "validator": "^13.5.2"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.12.10",
19 | "@babel/eslint-parser": "^7.14.7",
20 | "@babel/plugin-transform-runtime": "^7.12.10",
21 | "@babel/preset-env": "^7.12.11",
22 | "@shopify/eslint-plugin": "^40.4.0",
23 | "@shopify/themekit": "^1.1.7",
24 | "autoprefixer": "^10.3.1",
25 | "axios": "^0.21.1",
26 | "babel-loader": "^8.2.2",
27 | "babel-plugin-transform-class-properties": "^6.24.1",
28 | "browser-sync": "^2.26.14",
29 | "clean-css-cli": "^5.3.0",
30 | "clean-webpack-plugin": "^4.0.0-alpha.0",
31 | "copy-webpack-plugin": "6.x",
32 | "core-js": "^3.8.3",
33 | "css-loader": "^6.2.0",
34 | "debug": "^4.3.1",
35 | "eslint": "^7.31.0",
36 | "eslint-config-prettier": "^8.3.0",
37 | "eslint-webpack-plugin": "^3.0.1",
38 | "file-loader": "^6.2.0",
39 | "glob": "^7.1.6",
40 | "graphql": "^15.5.0",
41 | "html-webpack-plugin": "^5.3.2",
42 | "liquidjs": "^9.20.1",
43 | "mini-css-extract-plugin": "^2.1.0",
44 | "node-fetch": "^2.6.1",
45 | "node-sass": "^6.0.1",
46 | "postcss": "^8.3.6",
47 | "postcss-loader": "^6.1.1",
48 | "prettier": "^2.2.1",
49 | "raw-loader": "^4.0.2",
50 | "sass": "^1.32.5",
51 | "sass-loader": "^12.1.0",
52 | "style-loader": "^3.2.1",
53 | "tailwindcss": "^2.2.7",
54 | "transform-class-properties": "^1.0.0-beta",
55 | "typescript": "^4.1.3",
56 | "webpack": "^5.18.0",
57 | "webpack-bundle-analyzer": "^4.4.0",
58 | "webpack-cli": "^4.4.0",
59 | "webpack-dev-server": "^3.11.2",
60 | "webpack-merge": "^5.7.3",
61 | "webpack-shell-plugin-next": "^2.2.2",
62 | "yaml": "^1.10.0",
63 | "yargs": "^17.0.1"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 100,
3 | trailingComma: 'es5',
4 | singleQuote: true,
5 | tabWidth: 4
6 | };
7 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |      [](https://GitHub.com/3daddict/themekit-webpack/releases/)
2 |
3 | # (Legacy) Shopify ThemeKit with Webpack 5
4 | This is a starter Theme using Webpack, ThemeKit and TailwindCSS for developing Shopify themes with modern build tools. The goal is to create a tool with a component-based folder structure and is easy to use. With the new Shopify CLI a Webpack Dev Server is no loger needed. Please refer to this new project for Shopify 2.0 theme: [webpack-shopify-cli](https://github.com/3daddict/webpack-shopify-cli)
5 |
6 | ## Contributing
7 | To contribute to this repo please see
8 | - [CONTRIBUTING.md](https://github.com/3daddict/themekit-webpack/blob/master/CONTRIBUTING.md)
9 | - [CODE_OF_CONDUCT.md](https://github.com/3daddict/themekit-webpack/blob/master/CODE_OF_CONDUCT.md)
10 |
11 | ## 📁 Folder Structure
12 | This is set up for a component-based file structure. All liquid, js and scss are in the same folder to keep things in one place.
13 | Example of folder structure:
14 | * components
15 | * sections
16 | * header
17 | - header.js
18 | - header.liquid
19 | - header.scss
20 |
21 | **Note:** always import component scss files into the component javascript file. For example `import './header.scss';` is inside `header.js`. This ensures the scss will get compiled to css with Webpack.
22 | There are some rules to follow with this folder structure. For each component liquid file use the following template.
23 | ```html
24 |
25 |
26 |
27 | ```
28 | It's important to reference styles and scripts from the compiles assets folder. Webpack will compile all js and css files under the filename.
29 | For example a starting template for a `header.liquid` component would look like this.
30 | ```html
31 |
32 | {{ 'bundle.header.css' | asset_url | stylesheet_tag }}
33 |
34 |
37 |
38 |
39 | {{ 'bundle.header.js' | asset_url | script_tag }}
40 | ```
41 | **Note:** The scss or css file can alternatively be imported into the js file which will get compiled with webpack.
42 | ## Node Version Manager
43 | This theme setup is built with Yarn, Webpack and ThemeKit which are dependant on NodeJS versions.
44 | You can use node `v14` to install dependencies and run build commands.
45 | - Install [nvm](http://npm.github.io/installation-setup-docs/installing/using-a-node-version-manager.html)
46 | - Run `nvm install v14` in terminal
47 | - Install dependencies `yarn install`
48 |
49 | ## Clean-CSS CLI
50 | This project uses [clean-css-cli](https://www.npmjs.com/package/clean-css-cli) to minify TailwindCSS during build.
51 |
52 | Install Clean-CSS CLI
53 | `yarn add clean-css-cli -g`
54 | **Note:** Global install via -g option is recommended unless you want to execute the binary via a relative path, i.e. ./node_modules/.bin/cleancss
55 |
56 | ## TailwindCSS
57 | This project uses [TailwindCSS](https://tailwindcss.com/) `v2` a utility-first CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup. Checkout the amazing [documentation](https://tailwindcss.com/docs) and start adding classes to your elements.
58 |
59 | ## Getting Started
60 | - Install dependencies `yarn install` or `npm i`
61 | - Rename the `config.yml.example` to `config.yml` and add the Shopify Theme credentials
62 | - Run a build test `yarn build` if no errors then you are good to go
63 |
64 | **Note:** the first time you run `yarn start` or `yarn deploy` to a new theme you must comment out `ignore_files`.
65 | ```
66 | # - config/settings_data.json
67 | # - "*.js"
68 | # - "*.hot-update.json"
69 | ```
70 |
71 | ## Webpack Dev Server
72 | - `yarn start` will run build commands and start the webpack development server.
73 | - `yarn build` will run build commands and create a dist folder of the compiled files.
74 | - `yarn deploy` will upload the dist folder contents to your theme configured in the yml
75 |
76 | ## Self-Signed Certificate
77 | In the event that you find the HMR assets are not loading and the requests to localhost:9000 are 404 you will need to approve or pass a valid certificate.
78 | To solve this issue you can open a new browser window and approve the SSL Certificate or pass a valid certificate as mentioned here [devServer.https](https://webpack.js.org/configuration/dev-server/#devserverhttps).
79 |
80 | **Note:** a quick fix with Chrome `chrome://flags/#allow-insecure-localhost` change to enable
81 |
82 | ## HMR (Hot Module Reloading)
83 | When in development mode `yarn start` hot module reloading is enabled. It watches for changes to `JavaScript`, `CSS` and `Liquid` files. When JS or CSS is changes the browser will change without the need to refresh. When changes are made to liquid files a manual browser reload may be required.
84 |
85 | ## Whitespace control
86 | In [Liquid](https://shopify.github.io/liquid/basics/whitespace/), you can include a hyphen in your tag syntax `{{-`, `-}}`, `{%-`, and `-%}` to strip whitespace from the left or right side of a rendered tag.
87 | By including hyphens in your `assign` tag, you can strip the generated whitespace from the rendered template.
88 | If you don’t want any of your tags to print whitespace, as a general rule you can add hyphens to both sides of all your tags (`{%-` and `-%}`):
89 | ```html
90 | {%- assign username = "Borat Margaret Sagdiyev" -%}
91 | {%- if username and username.size > 10 -%}
92 | Wow, {{ username }}, I like!
93 | {%- else -%}
94 | Hello there!
95 | {%- endif -%}
96 | ```
97 |
98 | ## Variable Scope & Components
99 | This is not unique to this project but it's worth mentioning and creating a component example. See the `src/components/snippets/dynamic-modal/dynamic-modal.liquid` component. This is a simple modal that uses variable scope for data, styles and functions.
100 | Use this `dynamic-modal.liquid` component by creating a trigger element or function like a button with an id.
101 | ```html
102 | Trigger Modal
103 | ```
104 | Next we include the modal in a section with declared variables. These will be scoped to the snippet and we now have a dynamic reusable modal component we can use throughout our theme.
105 | ```html
106 | {%- render 'dynamic-modal',
107 | id: "testModal",
108 | openModalBtn: "modalBtn",
109 | title: "Modal Title",
110 | body: "Modal Body",
111 | buttonOne: "Alert",
112 | buttonOneFunction: "alert('Q: Do you struggle with impostor syndrome? Me: no I’m great at it')"
113 | buttonTwo: "Close",
114 | buttonTwoStyle: "text-white bg-red-500 hover:bg-red-700"
115 | buttonTwoFunction: "modal.style.display = 'none';"
116 | -%}
117 | ```
118 | I came across this idea using `include` from [David Warrington's](https://github.com/davidwarrington) article [Shopify Variable Scopes](https://ellodave.dev/blog/2019/5/24/shopify-variable-scopes/) and re-factored to use `render` tags. If you find some good use cases for these please post them in the [discussion ideas category](https://github.com/3daddict/themekit-webpack/discussions/categories/ideas)
119 |
--------------------------------------------------------------------------------
/shopify-dev-utils/convertToGlobalDataStructure.js:
--------------------------------------------------------------------------------
1 | module.exports.convertToGlobalDataStructure = function convertToGlobalDataStructure(gqlData) {
2 | // return gqlData;
3 | return {
4 | shop: {
5 | name: gqlData.shop.name,
6 | },
7 | collections: gqlData.collections.edges.map(({ node }) => ({
8 | title: node.title,
9 | id: node.id,
10 | handle: node.handle,
11 | image: node.image,
12 | description: node.description,
13 | url: `/collections/${node.handle}`,
14 | products: node.products.edges.map((product) => ({
15 | id: product.node.id,
16 | title: product.node.title,
17 | description: product.node.description,
18 | handle: product.node.handle,
19 | available: product.node.availableForSale,
20 | price: product.node.priceRange.maxVariantPrice, // preserve the entire obj for money-* filters
21 | price_max: product.node.priceRange.maxVariantPrice,
22 | price_min: product.node.priceRange.minVariantPrice,
23 | price_varies:
24 | +product.node.priceRange.maxVariantPrice.amount !==
25 | +product.node.priceRange.minVariantPrice,
26 | url: `/products/${product.node.handle}`,
27 | featured_image:
28 | product.node.images.edges.length > 0
29 | ? {
30 | id: product.node.images.edges[0].node.id,
31 | alt: product.node.images.edges[0].node.altText,
32 | src: product.node.images.edges[0].node.originalSrc,
33 | }
34 | : null,
35 | })),
36 | })),
37 | };
38 | };
39 |
--------------------------------------------------------------------------------
/shopify-dev-utils/filters/asset_url.js:
--------------------------------------------------------------------------------
1 | module.exports.assetUrl = function assetUrl(v) {
2 | const { publicPath } = this.context.opts.loaderOptions;
3 |
4 | return `${publicPath}${v}`;
5 | };
6 |
--------------------------------------------------------------------------------
/shopify-dev-utils/filters/default_pagination.js:
--------------------------------------------------------------------------------
1 | module.exports.defaultPagination = function defaultPagination(paginate, ...rest) {
2 | const next = rest.filter((arg) => arg[0] === 'next').pop();
3 | const previous = rest.filter((arg) => arg[0] === 'previous').pop();
4 |
5 | const prevLabel =
6 | previous.length > 0 ? previous.pop() : paginate.previous ? paginate.previous.title : '';
7 | const nextLabel = next.length > 0 ? next.pop() : paginate.next ? paginate.next.title : '';
8 |
9 | const prevPart = paginate.previous
10 | ? `${prevLabel} `
11 | : '';
12 | const nextPart = paginate.next
13 | ? `${nextLabel} `
14 | : '';
15 |
16 | const pagesPart = paginate.parts
17 | .map((part) => {
18 | if (part.is_link) {
19 | return `${part.title} `;
20 | }
21 | return `${part.title} `;
22 | })
23 | .join('');
24 |
25 | return `${prevPart}${pagesPart}${nextPart}`;
26 | };
27 |
--------------------------------------------------------------------------------
/shopify-dev-utils/filters/money_with_currency.js:
--------------------------------------------------------------------------------
1 | module.exports.moneyWithCurrency = function moneyWithCurrency(price) {
2 | if (!price || !price.currencyCode || !price.amount) {
3 | return '';
4 | }
5 |
6 | // the price that this object gets has 2 fields it is not the same value in "real" env,
7 | // at real it should be only a number multiplied by 100
8 | return new Intl.NumberFormat('en', {
9 | style: 'currency',
10 | currency: price.currencyCode,
11 | maximumFractionDigits: 2,
12 | }).format(price.amount);
13 | };
14 |
--------------------------------------------------------------------------------
/shopify-dev-utils/filters/money_without_trailing_zeros.js:
--------------------------------------------------------------------------------
1 | const { moneyWithCurrency } = require('./money_with_currency');
2 |
3 | module.exports.moneyWithoutTrailingZeros = function moneyWithoutTrailingZeros(price) {
4 | // the price that this object gets has 2 fields it is not the same value in "real" env,
5 | // at real it should be only a number multiplied by 100
6 | const moneyWithCurrencyAndTrailingZeros = moneyWithCurrency(price);
7 | return moneyWithCurrencyAndTrailingZeros.replace(
8 | /([,.][^0]*(0+))\D*$/,
9 | (match, group, zeros) => {
10 | const cutSize = zeros.length > 1 ? zeros.length + 1 : zeros.length;
11 | return match.replace(group, group.substring(0, group.length - cutSize));
12 | }
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/shopify-dev-utils/filters/script_tag.js:
--------------------------------------------------------------------------------
1 | module.exports.scriptTag = function scriptTag(v) {
2 | return ``;
3 | };
4 |
--------------------------------------------------------------------------------
/shopify-dev-utils/filters/stylesheet_tag.js:
--------------------------------------------------------------------------------
1 | module.exports.stylesheetTag = function stylesheetTag() {
2 | return ''; // in Dev mode we load css from js for HMR
3 | };
4 |
--------------------------------------------------------------------------------
/shopify-dev-utils/filters/within.js:
--------------------------------------------------------------------------------
1 | module.exports.within = function within(productUrl, collection) {
2 | return `${collection.url}${productUrl}`;
3 | };
4 |
--------------------------------------------------------------------------------
/shopify-dev-utils/liquidDev.entry.js:
--------------------------------------------------------------------------------
1 | const context = require.context(
2 | '../src',
3 | true,
4 | /(collection|footer|featured-product|featured-collection|header|message)\.liquid$/
5 | );
6 |
7 | const cache = {};
8 |
9 | context.keys().forEach(function (key) {
10 | cache[key] = context(key);
11 | });
12 |
13 | function replaceHtml(key, startCommentNode) {
14 | const commentNodeType = startCommentNode.nodeType;
15 | while (
16 | startCommentNode.nextSibling.nodeType !== commentNodeType ||
17 | !startCommentNode.nextSibling.nodeValue.includes(`hmr-end: ${key}`)
18 | ) {
19 | startCommentNode.nextSibling.remove();
20 | }
21 |
22 | const tpl = document.createElement('template');
23 | tpl.innerHTML = cache[key];
24 | startCommentNode.parentNode.insertBefore(tpl.content, startCommentNode.nextSibling);
25 | }
26 |
27 | if (module.hot) {
28 | module.hot.accept(context.id, function () {
29 | const newContext = require.context(
30 | '../src',
31 | true,
32 | /(collection|footer|featured-product|featured-collection|header|message)\.liquid$/
33 | );
34 | const changes = [];
35 | newContext.keys().forEach(function (key) {
36 | const newFile = newContext(key);
37 | if (cache[key] !== newFile) {
38 | changes.push(key);
39 | cache[key] = newFile;
40 | }
41 | });
42 |
43 | changes.forEach((changedFile) => {
44 | traverseHMRComments(changedFile, replaceHtml);
45 | });
46 | });
47 | }
48 |
49 | function traverseHMRComments(file, callback) {
50 | const nodeIterator = document.createNodeIterator(
51 | document.body,
52 | NodeFilter.SHOW_COMMENT,
53 | function (node) {
54 | return node.nodeValue.includes(`hmr-start: ${file}`)
55 | ? NodeFilter.FILTER_ACCEPT
56 | : NodeFilter.FILTER_REJECT;
57 | }
58 | );
59 |
60 | while (nodeIterator.nextNode()) {
61 | callback(file, nodeIterator.referenceNode);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/shopify-dev-utils/liquidDev.loader.js:
--------------------------------------------------------------------------------
1 | const loaderUtils = require('loader-utils');
2 | const path = require('path');
3 | const { Liquid } = require('liquidjs');
4 | const glob = require('glob');
5 | const { moneyWithoutTrailingZeros } = require('./filters/money_without_trailing_zeros');
6 | const { moneyWithCurrency } = require('./filters/money_with_currency');
7 | const { within } = require('./filters/within');
8 | const { defaultPagination } = require('./filters/default_pagination');
9 | const { scriptTag } = require('./filters/script_tag');
10 | const { stylesheetTag } = require('./filters/stylesheet_tag');
11 | const { assetUrl } = require('./filters/asset_url');
12 | const { getStoreGlobalData } = require('./storeData');
13 | const { liquidSectionTags } = require('./section-tags/index');
14 | const { Paginate } = require('./tags/paginate');
15 |
16 | let engine;
17 | let loadPromise;
18 |
19 | function initEngine() {
20 | if (!loadPromise) {
21 | loadPromise = new Promise(async (resolve) => {
22 | const liquidFiles = [
23 | ...glob
24 | .sync('./src/components/**/*.liquid')
25 | .map((filePath) =>
26 | path.resolve(path.join(__dirname, '../'), path.dirname(filePath))
27 | )
28 | .reduce((set, dir) => {
29 | set.add(dir);
30 | return set;
31 | }, new Set()),
32 | ];
33 |
34 | engine = new Liquid({
35 | root: liquidFiles, // root for layouts/includes lookup
36 | extname: '.liquid', // used for layouts/includes, defaults "",
37 | globals: await getStoreGlobalData(),
38 | });
39 |
40 | engine.registerFilter('asset_url', assetUrl);
41 | engine.registerFilter('stylesheet_tag', stylesheetTag);
42 | engine.registerFilter('script_tag', scriptTag);
43 | engine.registerFilter('default_pagination', defaultPagination);
44 | engine.registerFilter('within', within);
45 | engine.registerFilter('money_with_currency', moneyWithCurrency);
46 | engine.registerFilter('money_without_trailing_zeros', moneyWithoutTrailingZeros);
47 |
48 | engine.registerTag('paginate', Paginate);
49 | engine.plugin(liquidSectionTags());
50 |
51 | resolve();
52 | });
53 | }
54 |
55 | return loadPromise;
56 | }
57 |
58 | module.exports = async function (content) {
59 | if (this.cacheable) this.cacheable();
60 | const callback = this.async();
61 |
62 | if (!engine) {
63 | await initEngine();
64 | }
65 |
66 | engine.options.loaderOptions = loaderUtils.getOptions(this);
67 | const { isSection } = engine.options.loaderOptions;
68 |
69 | // section handled specially
70 | if (typeof isSection === 'function' && isSection(this.context)) {
71 | const sectionName = path.basename(this.resourcePath, '.liquid');
72 | content = `{% section "${sectionName}" %}`;
73 | }
74 |
75 | return engine
76 | .parseAndRender(content, engine.options.loaderOptions.globals || {})
77 | .then((result) => callback(null, result));
78 | };
79 |
--------------------------------------------------------------------------------
/shopify-dev-utils/section-tags/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Liquid } from 'liquidjs';
2 | export declare function liquidSectionTags(): (this: Liquid) => void;
3 |
--------------------------------------------------------------------------------
/shopify-dev-utils/section-tags/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.liquidSectionTags = void 0;
4 | const javascript_1 = require("./javascript");
5 | const schema_1 = require("./schema");
6 | const section_1 = require("./section");
7 | const stylesheet_1 = require("./stylesheet");
8 | function liquidSectionTags() {
9 | return function () {
10 | this.registerTag('section', section_1.Section);
11 | this.registerTag('schema', schema_1.Schema);
12 | this.registerTag('stylesheet', stylesheet_1.StyleSheet);
13 | this.registerTag('javascript', javascript_1.JavaScript);
14 | };
15 | }
16 | exports.liquidSectionTags = liquidSectionTags;
17 |
--------------------------------------------------------------------------------
/shopify-dev-utils/section-tags/javascript.d.ts:
--------------------------------------------------------------------------------
1 | import { TagImplOptions } from 'liquidjs/dist/template/tag/tag-impl-options';
2 | export declare const JavaScript: TagImplOptions;
3 |
--------------------------------------------------------------------------------
/shopify-dev-utils/section-tags/javascript.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.JavaScript = void 0;
4 | exports.JavaScript = {
5 | parse: function (tagToken, remainTokens) {
6 | this.tokens = [];
7 | const stream = this.liquid.parser.parseStream(remainTokens);
8 | stream
9 | .on('token', (token) => {
10 | if (token.name === 'endjavascript')
11 | stream.stop();
12 | else
13 | this.tokens.push(token);
14 | })
15 | .on('end', () => {
16 | throw new Error(`tag ${tagToken.getText()} not closed`);
17 | });
18 | stream.start();
19 | },
20 | render: function () {
21 | const text = this.tokens.map((token) => token.getText()).join('');
22 | return ``;
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/shopify-dev-utils/section-tags/schema.d.ts:
--------------------------------------------------------------------------------
1 | import { TagImplOptions } from 'liquidjs/dist/template/tag/tag-impl-options';
2 | export declare const Schema: TagImplOptions;
3 |
--------------------------------------------------------------------------------
/shopify-dev-utils/section-tags/schema.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.Schema = void 0;
4 | function generateSettingsObj(settings) {
5 | if (!Array.isArray(settings)) {
6 | return settings;
7 | }
8 | return settings
9 | .filter((entry) => !!entry.id)
10 | .reduce((sectionSettings, entry) => {
11 | sectionSettings[entry.id] = entry.default;
12 | return sectionSettings;
13 | }, {});
14 | }
15 | exports.Schema = {
16 | parse: function (tagToken, remainTokens) {
17 | this.tokens = [];
18 | const stream = this.liquid.parser.parseStream(remainTokens);
19 | stream
20 | .on('token', (token) => {
21 | if (token.name === 'endschema') {
22 | stream.stop();
23 | }
24 | else
25 | this.tokens.push(token);
26 | })
27 | .on('end', () => {
28 | throw new Error(`tag ${tagToken.getText()} not closed`);
29 | });
30 | stream.start();
31 | },
32 | render: function (ctx) {
33 | const json = this.tokens.map((token) => token.getText()).join('');
34 | const schema = JSON.parse(json);
35 | const scope = ctx.scopes[ctx.scopes.length - 1];
36 | scope.section = {
37 | settings: generateSettingsObj(schema.settings),
38 | blocks: (schema.blocks || []).map((block) => ({
39 | ...block,
40 | settings: generateSettingsObj(block.settings)
41 | }))
42 | };
43 | return '';
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/shopify-dev-utils/section-tags/section.d.ts:
--------------------------------------------------------------------------------
1 | import { TagImplOptions } from 'liquidjs';
2 | export declare const Section: TagImplOptions;
3 |
--------------------------------------------------------------------------------
/shopify-dev-utils/section-tags/section.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.Section = void 0;
4 | const quoted = /^'[^']*'|"[^"]*"$/;
5 | exports.Section = {
6 | parse: function (token) {
7 | this.namestr = token.args;
8 | },
9 | render: function* (ctx, emitter) {
10 | let name;
11 | if (quoted.exec(this.namestr)) {
12 | const template = this.namestr.slice(1, -1);
13 | name = yield this.liquid._parseAndRender(template, ctx.getAll(), ctx.opts);
14 | }
15 | if (!name)
16 | throw new Error('cannot include with empty filename');
17 | const templates = yield this.liquid._parseFile(name, ctx.opts, ctx.sync);
18 | // Bubble up schema tag for allowing it's data available to the section
19 | templates.sort((tagA) => {
20 | return tagA.token.kind === 4 &&
21 | tagA.token.name === 'schema'
22 | ? -1
23 | : 0;
24 | });
25 | const scope = {};
26 | ctx.push(scope);
27 | yield this.liquid.renderer.renderTemplates(templates, ctx, emitter);
28 | ctx.pop();
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/shopify-dev-utils/section-tags/stylesheet.d.ts:
--------------------------------------------------------------------------------
1 | import { TagImplOptions } from 'liquidjs';
2 | export declare const StyleSheet: TagImplOptions;
3 |
--------------------------------------------------------------------------------
/shopify-dev-utils/section-tags/stylesheet.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.StyleSheet = void 0;
4 | const sass_1 = require("sass");
5 | const quoted = /^'[^']*'|"[^"]*"$/;
6 | const processors = {
7 | '': (x) => x,
8 | sass: sassProcessor,
9 | scss: sassProcessor
10 | };
11 | exports.StyleSheet = {
12 | parse: function (token, remainTokens) {
13 | this.processor = token.args;
14 | this.tokens = [];
15 | const stream = this.liquid.parser.parseStream(remainTokens);
16 | stream
17 | .on('token', (token) => {
18 | if (token.name === 'endstylesheet')
19 | stream.stop();
20 | else
21 | this.tokens.push(token);
22 | })
23 | .on('end', () => {
24 | throw new Error(`tag ${token.getText()} not closed`);
25 | });
26 | stream.start();
27 | },
28 | render: async function (ctx) {
29 | let processor = '';
30 | if (quoted.exec(this.processor)) {
31 | const template = this.processor.slice(1, -1);
32 | processor = await this.liquid.parseAndRender(template, ctx.getAll(), ctx.opts);
33 | }
34 | const text = this.tokens.map((token) => token.getText()).join('');
35 | const p = processors[processor];
36 | if (!p)
37 | throw new Error(`processor for ${processor} not found`);
38 | const css = await p(text);
39 | return ``;
40 | }
41 | };
42 | function sassProcessor(data) {
43 | return new Promise((resolve, reject) => sass_1.render({ data }, (err, result) => err ? reject(err) : resolve('' + result.css)));
44 | }
45 |
--------------------------------------------------------------------------------
/shopify-dev-utils/storeData.js:
--------------------------------------------------------------------------------
1 | const yaml = require('yaml');
2 | const fs = require('fs');
3 | const path = require('path');
4 | const { convertToGlobalDataStructure } = require('./convertToGlobalDataStructure');
5 | const { StorefrontApi } = require('./storefrontApi');
6 |
7 | const configFile = path.join(__dirname, '../config.yml');
8 | let config = { token: '', baseURL: '' };
9 | if (fs.existsSync(configFile)) {
10 | const configYml = yaml.parse(fs.readFileSync(configFile, 'utf-8'));
11 | config.token = configYml.development.storefront_api_key;
12 | config.baseURL = configYml.development.store;
13 |
14 | if (!config.token) {
15 | console.warn(`'storefront_api_key' was not found in 'config.yml'`);
16 | }
17 | }
18 |
19 | function getGlobalSettings() {
20 | const rawSettings = require('../src/config/settings_schema.json');
21 | const overrides = { environment: 'development' };
22 |
23 | return rawSettings
24 | .filter((section) => !!section.settings)
25 | .reduce((result, section) => {
26 | section.settings
27 | .filter((setting) => !!setting.id && typeof setting.default !== 'undefined')
28 | .forEach((setting) => {
29 | result[setting.id] = overrides[setting.id] || setting.default;
30 | });
31 | return result;
32 | }, {});
33 | }
34 |
35 | async function getStoreGlobalData() {
36 | const storefrontApi = new StorefrontApi(config);
37 |
38 | const data = await storefrontApi
39 | .getStoreData()
40 | .then(({ data }) => convertToGlobalDataStructure(data));
41 |
42 | return {
43 | shop: {
44 | name: data.shop.name,
45 | },
46 | settings: getGlobalSettings(),
47 | linklists: {
48 | 'main-menu': {
49 | title: '',
50 | levels: 1,
51 | links: [
52 | {
53 | title: 'Home',
54 | url: '/',
55 | links: [],
56 | },
57 | {
58 | title: 'Catalog',
59 | url: '/collections/all',
60 | links: [],
61 | },
62 | ],
63 | },
64 | },
65 | collection: data.collections[0],
66 | };
67 | }
68 |
69 | module.exports.getStoreGlobalData = getStoreGlobalData;
70 |
--------------------------------------------------------------------------------
/shopify-dev-utils/storefrontApi.js:
--------------------------------------------------------------------------------
1 | const Axios = require('axios');
2 |
3 | class StorefrontApi {
4 | constructor({ baseURL, token }) {
5 | console.log(`https://${baseURL}/api/2020-10/graphql`);
6 | this.axios = Axios.create({
7 | baseURL: `https://${baseURL}/api/2020-10/graphql`,
8 | headers: {
9 | Accept: 'application/json',
10 | 'Content-Type': 'application/graphql',
11 | 'X-Shopify-Storefront-Access-Token': token,
12 | },
13 | });
14 | }
15 |
16 | async getStoreData() {
17 | return this.axios
18 | .post(
19 | '',
20 | `
21 | {
22 | shop {
23 | name
24 | }
25 | collections(first: 50) {
26 | edges {
27 | node {
28 | id
29 | title
30 | handle
31 | description
32 | image(scale:1) {
33 | id
34 | altText
35 | originalSrc
36 | transformedSrc
37 | }
38 | products(first: 50) {
39 | edges {
40 | node {
41 | id
42 | title
43 | description
44 | handle
45 | availableForSale
46 | priceRange {
47 | maxVariantPrice {
48 | amount
49 | currencyCode
50 | }
51 | minVariantPrice {
52 | amount
53 | currencyCode
54 | }
55 | }
56 | images(first: 1) {
57 | edges {
58 | node {
59 | id
60 | altText
61 | originalSrc
62 | }
63 | }
64 | }
65 | onlineStoreUrl
66 | }
67 | }
68 | }
69 | }
70 | }
71 | }
72 | }
73 | `
74 | )
75 | .then(({ data }) => data)
76 | .catch(err => {
77 | console.log(err);
78 | res.sendStatus(500);
79 | });
80 | }
81 | }
82 |
83 | module.exports.StorefrontApi = StorefrontApi;
84 |
--------------------------------------------------------------------------------
/shopify-dev-utils/tags/paginate.d.ts:
--------------------------------------------------------------------------------
1 | import { TagImplOptions } from 'liquidjs/dist/template/tag/tag-impl-options';
2 | export declare const Paginate: TagImplOptions;
3 |
--------------------------------------------------------------------------------
/shopify-dev-utils/tags/paginate.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.Paginate = void 0;
4 | const liquidjs_1 = require("liquidjs");
5 | function generatePaginateObj({ offset, perPage, total }) {
6 | const pages = Math.ceil(total / perPage);
7 | const currentPage = Math.floor((offset + perPage) / perPage);
8 | const paginate = {
9 | current_offset: offset,
10 | current_page: currentPage,
11 | items: total,
12 | page_size: perPage,
13 | parts: Array(pages)
14 | .fill(0)
15 | .map((_, index) => {
16 | const page = index + 1;
17 | if (page === currentPage) {
18 | return { title: page, is_link: false };
19 | }
20 | return { title: page, url: `?page=${page}`, is_link: true };
21 | }),
22 | pages,
23 | previous: undefined,
24 | next: undefined
25 | };
26 | if (currentPage === pages && pages > 1) {
27 | paginate.previous = {
28 | title: '\u0026laquo; Previous',
29 | url: `?page=${currentPage - 1}`,
30 | is_link: true
31 | };
32 | }
33 | else if (currentPage < pages && pages > 1) {
34 | paginate.next = {
35 | title: 'Next \u0026raquo;',
36 | url: `?page=${currentPage + 1}`,
37 | is_link: true
38 | };
39 | }
40 | return paginate;
41 | }
42 | function populateVariableObj({ list, originalValue, depth }) {
43 | if (depth.length === 0) {
44 | return list;
45 | }
46 | const clone = JSON.parse(JSON.stringify(originalValue));
47 | depth.reduce((result, prop, index) => {
48 | const propName = prop.getText();
49 | if (index === depth.length - 1) {
50 | result[propName] = list;
51 | }
52 | return result[propName] || {};
53 | }, clone);
54 | return clone;
55 | }
56 | exports.Paginate = {
57 | parse: function (tagToken, remainTokens) {
58 | this.templates = [];
59 | const stream = this.liquid.parser.parseStream(remainTokens);
60 | stream
61 | .on('start', () => {
62 | const toknenizer = new liquidjs_1.Tokenizer(tagToken.args);
63 | const list = toknenizer.readValue();
64 | const by = toknenizer.readIdentifier();
65 | const perPage = toknenizer.readValue();
66 | liquidjs_1.assert(list.size() &&
67 | by.content === 'by' &&
68 | +perPage.getText() > 0 &&
69 | +perPage.getText() <= 50, () => `illegal tag: ${tagToken.getText()}`);
70 | this.args = { list, perPage: +perPage.getText() };
71 | })
72 | .on('tag:endpaginate', () => stream.stop())
73 | .on('template', (tpl) => {
74 | this.templates.push(tpl);
75 | })
76 | .on('end', () => {
77 | throw new Error(`tag ${tagToken.getText()} not closed`);
78 | });
79 | stream.start();
80 | },
81 | render: function* (ctx, emitter) {
82 | const list = yield liquidjs_1.evalToken(this.args.list, ctx) || [];
83 | const perPage = this.args.perPage;
84 | const currentPage = +ctx.get(['current_page']);
85 | const offset = currentPage ? (currentPage - 1) * perPage : 0;
86 | const variableName = this.args.list.getVariableAsText();
87 | const originalValue = ctx.get([variableName]);
88 | const scopeList = list.slice(offset, offset + perPage);
89 | const data = populateVariableObj({
90 | list: scopeList,
91 | originalValue,
92 | depth: this.args.list.props
93 | });
94 | const paginate = generatePaginateObj({
95 | offset,
96 | perPage,
97 | total: list.length
98 | });
99 | const scope = { [variableName]: data, paginate };
100 | ctx.push(scope);
101 | yield this.liquid.renderer.renderTemplates(this.templates, ctx, emitter);
102 | ctx.pop();
103 | }
104 | };
105 |
--------------------------------------------------------------------------------
/shopify-dev-utils/transformLiquid.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports.transformLiquid = function transformLiquid(publicPath) {
4 | return (content, absolutePath) => {
5 | const relativePath = path.join(__dirname, '../src');
6 | const diff = path.relative(relativePath, absolutePath);
7 |
8 | content = content
9 | .toString()
10 | .replace(
11 | /{{\s*'([^']+)'\s*\|\s*asset_url\s*\|\s*(stylesheet_tag|script_tag)\s*}}/g,
12 | function (matched, fileName, type) {
13 | if (type === 'stylesheet_tag') {
14 | if (fileName !== 'tailwind.min.css') {
15 | return '';
16 | }
17 | return matched;
18 | }
19 |
20 | return ``;
21 | }
22 | );
23 |
24 | if(diff.includes('/layout/theme.liquid')) {
25 | // inject HMR entry bundle
26 | content = content.replace('',``)
27 | }
28 |
29 | return `${content}`;
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/src/assets/favicon/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3daddict/themekit-webpack/5f05ecbb27a7493d4bb01cb24035fa1b4145208f/src/assets/favicon/.gitkeep
--------------------------------------------------------------------------------
/src/assets/fonts/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3daddict/themekit-webpack/5f05ecbb27a7493d4bb01cb24035fa1b4145208f/src/assets/fonts/.gitkeep
--------------------------------------------------------------------------------
/src/assets/fonts/noto-serif/NotoSerif-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3daddict/themekit-webpack/5f05ecbb27a7493d4bb01cb24035fa1b4145208f/src/assets/fonts/noto-serif/NotoSerif-Bold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/noto-serif/NotoSerif-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3daddict/themekit-webpack/5f05ecbb27a7493d4bb01cb24035fa1b4145208f/src/assets/fonts/noto-serif/NotoSerif-Regular.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/poppins/Poppins-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3daddict/themekit-webpack/5f05ecbb27a7493d4bb01cb24035fa1b4145208f/src/assets/fonts/poppins/Poppins-ExtraBold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/poppins/Poppins-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3daddict/themekit-webpack/5f05ecbb27a7493d4bb01cb24035fa1b4145208f/src/assets/fonts/poppins/Poppins-Light.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/poppins/Poppins-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3daddict/themekit-webpack/5f05ecbb27a7493d4bb01cb24035fa1b4145208f/src/assets/fonts/poppins/Poppins-Regular.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/poppins/Poppins-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3daddict/themekit-webpack/5f05ecbb27a7493d4bb01cb24035fa1b4145208f/src/assets/fonts/poppins/Poppins-SemiBold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/poppins/Poppins-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3daddict/themekit-webpack/5f05ecbb27a7493d4bb01cb24035fa1b4145208f/src/assets/fonts/poppins/Poppins-Thin.ttf
--------------------------------------------------------------------------------
/src/assets/images/great-success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/3daddict/themekit-webpack/5f05ecbb27a7493d4bb01cb24035fa1b4145208f/src/assets/images/great-success.png
--------------------------------------------------------------------------------
/src/assets/svg/webpack.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/helpers/cart-fetch-api/cart-fetch-api.js:
--------------------------------------------------------------------------------
1 | const base = '/cart';
2 |
3 | function send({method, path, data}) {
4 |
5 | const fetch = require('node-fetch').default;
6 |
7 | const opts = {method, headers: {}};
8 |
9 | if (data) {
10 | opts.headers['Content-Type'] = 'application/json';
11 | opts.body = JSON.stringify(data);
12 | }
13 |
14 | return fetch(`${base}/${path}`, opts)
15 | .then(res => res.text())
16 | .then(json => {
17 | try {
18 | return JSON.parse(json);
19 | } catch (err) {
20 | return json;
21 | }
22 | });
23 | }
24 |
25 | export function get(path) {
26 | return send({method: 'GET', path});
27 | }
28 |
29 | export function del(path) {
30 | return send({method: 'DELETE', path});
31 | }
32 |
33 | export function post(path, data) {
34 | return send({method: 'POST', path, data});
35 | }
36 |
37 | export function put(path, data) {
38 | return send({method: 'PUT', path, data});
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/layout/theme.js:
--------------------------------------------------------------------------------
1 | import '../tailwind.css';
2 |
3 | const themeFunction = () => 'Theme JS ES6 Function!';
4 | console.log(themeFunction());
5 |
--------------------------------------------------------------------------------
/src/components/layout/theme.liquid:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {%- if page_description -%}
8 |
9 | {%- endif -%}
10 |
11 |
12 | {%- if settings.favicon != blank -%}
13 |
14 | {%- endif -%}
15 |
16 | {%- capture seo_title -%}
17 | {{ page_title }}
18 | {%- if current_tags -%}
19 | {%- assign meta_tags = current_tags | join: ', ' -%} – {{ 'general.meta.tags' | t: tags: meta_tags -}}
20 | {%- endif -%}
21 | {%- if current_page != 1 -%}
22 | – {{ 'general.meta.page' | t: page: current_page }}
23 | {%- endif -%}
24 | {%- assign escaped_page_title = page_title | escape -%}
25 | {%- unless escaped_page_title contains shop.name -%}
26 | – {{ shop.name }}
27 | {%- endunless -%}
28 | {%- endcapture -%}
29 | {{ seo_title | strip }}
30 |
31 | {%- comment -%}Varibles{%- endcomment -%}
32 | {{ content_for_header }}
33 | {% render 'global-css' %}
34 |
35 | {{ 'tailwind.min.css' | asset_url | stylesheet_tag }}
36 | {{ 'bundle.global-css.css' | asset_url | stylesheet_tag }}
37 | {{ 'bundle.runtime.js' | asset_url | script_tag }}
38 | {% if settings.environment == 'development' %}
39 | {{ 'bundle.liquidDev.js' | asset_url | script_tag }}
40 | {% endif %}
41 | {{ 'bundle.theme.js' | asset_url | script_tag }}
42 |
43 |
44 | {% section 'header' %}
45 |
46 | {% if shop.customer_accounts_enabled %}
47 | {% if customer %}
48 | account
49 | {{ 'log out' | customer_logout_link }}
50 | {% else %}
51 | {{ 'log in ' | customer_login_link }}
52 | {{ 'register' | customer_register_link }}
53 | {% endif %}
54 | {% endif %}
55 |
56 |
57 | {{ content_for_layout }}
58 |
59 |
60 | {% section 'footer' %}
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/components/sections/banner-with-text/banner-with-text.liquid:
--------------------------------------------------------------------------------
1 |
2 | {%- if section.settings.image != blank -%}
3 |
4 |
5 |
6 |
10 |
11 | {%- else -%}
12 |
13 | {%- endif -%}
14 | {%- if section.settings.overlay -%}
15 |
16 | {%- endif -%}
17 |
18 |
{{ section.settings.title }}
19 |
{{ section.settings.text }}
20 | {%- if section.blocks -%}
21 |
28 | {%- endif -%}
29 |
30 |
31 |
32 | {% schema %}
33 | {
34 | "name": "Banner with Text",
35 | "max_blocks": 2,
36 | "settings": [
37 | {
38 | "type": "image_picker",
39 | "id": "image",
40 | "label": "Banner Image"
41 | },
42 | {
43 | "type": "checkbox",
44 | "id": "overlay",
45 | "label": "Image Overlay",
46 | "info": "Darken Image with Black Overlay",
47 | "default": true
48 | },
49 | {
50 | "type": "url",
51 | "id": "banner_link",
52 | "label": "Banner Link"
53 | },
54 | {
55 | "type": "text",
56 | "id": "title",
57 | "label": "Banner Heading",
58 | "default": "ThemeKit with Webpack"
59 | },
60 | {
61 | "type": "textarea",
62 | "id": "text",
63 | "label": "Banner Body",
64 | "default": "A modern Shopify Theme with Webpack and TailwindCSS"
65 | }
66 | ],
67 | "blocks": [
68 | {
69 | "type": "select",
70 | "name": "CTA Button",
71 | "settings": [
72 | {
73 | "label": "Button Text",
74 | "id": "text",
75 | "type": "text",
76 | "default": "CTA Button"
77 | },
78 | {
79 | "label": "Button Link",
80 | "id": "link",
81 | "type": "url"
82 | }
83 | ]
84 | }
85 | ],
86 | "presets": [
87 | {
88 | "name": "Banner with Text",
89 | "category": "Image"
90 | }
91 | ]
92 | }
93 | {% endschema %}
--------------------------------------------------------------------------------
/src/components/sections/cart/cart.liquid:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/sections/feature-collection/featured-collection.liquid:
--------------------------------------------------------------------------------
1 | {%- assign collection = collections[section.settings.collection] -%}
2 |
3 |
4 |
5 |
6 | {%- if section.settings.title != blank -%}
7 |
{{ section.settings.title | escape }}
8 | {%- endif -%}
9 | {%- if section.settings.description != blank -%}
10 |
{{ section.settings.description | escape }}
11 | {%- endif -%}
12 |
19 |
20 |
21 | {%- if collection.image -%}
22 |
24 | {%- else -%}
25 |
27 | {%- endif -%}
28 |
29 |
30 |
31 |
32 |
33 | {% schema %}
34 | {
35 | "name": "Featured collection",
36 | "settings": [
37 | {
38 | "type": "text",
39 | "id": "title",
40 | "label": "Heading",
41 | "default": "Featured Collection"
42 | },
43 | {
44 | "type": "text",
45 | "id": "description",
46 | "label": "Description",
47 | "default": "There are some cool Tailwind CSS components you can use with this project."
48 | },
49 | {
50 | "type": "text",
51 | "id": "button",
52 | "label": "CTA Text",
53 | "default": "TailwindCSS Components"
54 | },
55 | {
56 | "type": "url",
57 | "id": "button_link",
58 | "label": "Button URL"
59 | },
60 | {
61 | "id": "collection",
62 | "type": "collection",
63 | "label": "Collection"
64 | }
65 | ],
66 | "presets": [
67 | {
68 | "name": "Featured collection",
69 | "category": "Collection"
70 | }
71 | ]
72 | }
73 | {% endschema %}
74 |
--------------------------------------------------------------------------------
/src/components/sections/featured-product/featured-product.liquid:
--------------------------------------------------------------------------------
1 | {%- assign product = all_products[section.settings.product] -%}
2 | {%- assign featured_media = product.selected_or_first_available_variant.featured_media | default: product.featured_media -%}
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
{{ product.title | escape }}
11 |
{{ product.price | money }}
12 |
13 |
14 |
Count:
15 |
16 |
17 |
18 |
19 |
20
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Color:
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Order Now
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | {% schema %}
45 | {
46 | "name": "Featured product",
47 | "settings": [
48 | {
49 | "type": "product",
50 | "id": "product",
51 | "label": "Product"
52 | }
53 | ],
54 | "presets": [
55 | {
56 | "name": "Featured product",
57 | "category": "Product"
58 | }
59 | ]
60 | }
61 | {% endschema %}
--------------------------------------------------------------------------------
/src/components/sections/footer/footer.liquid:
--------------------------------------------------------------------------------
1 |
69 |
70 | {% schema %}
71 | {
72 | "name": "Footer",
73 | "settings": [
74 | {
75 | "type": "header",
76 | "content": "First Menu"
77 | },
78 | {
79 | "type": "checkbox",
80 | "id": "first_menu_enable",
81 | "label": "Enable Menu",
82 | "default": true
83 | },
84 | {
85 | "type": "text",
86 | "id": "first_title",
87 | "label": "Menu Title",
88 | "default": "Menu 1"
89 | },
90 | {
91 | "type": "link_list",
92 | "id": "first_linklist",
93 | "label": "First menu"
94 | },
95 | {
96 | "type": "header",
97 | "content": "Second Menu"
98 | },
99 | {
100 | "type": "checkbox",
101 | "id": "second_menu_enable",
102 | "label": "Enable Menu",
103 | "default": true
104 | },
105 | {
106 | "type": "text",
107 | "id": "second_title",
108 | "label": "Menu Title",
109 | "default": "Menu 2"
110 | },
111 | {
112 | "type": "link_list",
113 | "id": "second_linklist",
114 | "label": "Second menu"
115 | },
116 | {
117 | "type": "header",
118 | "content": "Third Menu"
119 | },
120 | {
121 | "type": "checkbox",
122 | "id": "third_menu_enable",
123 | "label": "Enable Menu",
124 | "default": true
125 | },
126 | {
127 | "type": "text",
128 | "id": "third_title",
129 | "label": "Menu Title",
130 | "default": "Menu 3"
131 | },
132 | {
133 | "type": "link_list",
134 | "id": "third_linklist",
135 | "label": "Third menu"
136 | }
137 | ]
138 | }
139 | {% endschema %}
140 |
--------------------------------------------------------------------------------
/src/components/sections/header/header.liquid:
--------------------------------------------------------------------------------
1 |
2 | {%- if section.settings.announcement_bar_enabled -%}
3 |
4 | {%- if section.settings.announcement_bar_link != blank -%}
5 |
6 | {{ section.settings.announcement_bar_text }}
7 |
8 | {%- else -%}
9 |
{{ section.settings.announcement_bar_text }}
10 | {%- endif -%}
11 |
12 | {%- endif -%}
13 |
14 |
15 | {%- if section.settings.logo != blank -%}
16 |
24 | {%- endif -%}
25 |
{{ shop.name }}
26 |
27 |
28 |
29 | {%- for link in linklists[section.settings.menu].links -%}
30 | {%- if link.links != blank -%}
31 |
32 | {{ link.title }}
33 |
34 | {%- for childlink in link.links -%}
35 | {{ childlink.title }}
36 |
37 | {%- endfor -%}
38 |
39 |
40 | {%- else -%}
41 | {{ link.title }}
42 | {%- endif -%}
43 | {%- endfor -%}
44 |
45 |
46 |
47 |
48 |
49 | {% schema %}
50 | {
51 | "name": "Header",
52 | "settings": [{
53 | "type": "header",
54 | "content": "Announcement Bar"
55 | },
56 | {
57 | "type": "checkbox",
58 | "id": "announcement_bar_enabled",
59 | "label": "Show Announcement",
60 | "default": true
61 | },
62 | {
63 | "type": "text",
64 | "id": "announcement_bar_text",
65 | "label": "Announcement Text",
66 | "default": "Webpack is awesome!"
67 | },
68 | {
69 | "type": "color",
70 | "id": "announcement_bar_bg",
71 | "label": "Background Color",
72 | "default": "#000000"
73 | },
74 | {
75 | "type": "color",
76 | "id": "announcement_bar_text_color",
77 | "label": "Annoucement Text Color",
78 | "default": "#FFFFFF"
79 | },
80 | {
81 | "type": "url",
82 | "id": "announcement_bar_link",
83 | "label": "Announcement Link",
84 | "info": "Optional"
85 | },
86 | {
87 | "type": "header",
88 | "content": "Navbar"
89 | },
90 | {
91 | "type": "color",
92 | "id": "nav_bar_bg",
93 | "label": "Navbar Background Color",
94 | "default": "#FFFFFF"
95 | },
96 | {
97 | "type": "color",
98 | "id": "nav_bar_text_color",
99 | "label": "Navbar Text Color",
100 | "default": "#000000"
101 | },
102 | {
103 | "type": "image_picker",
104 | "id": "logo",
105 | "label": "Logo Image"
106 | },
107 | {
108 | "type": "range",
109 | "id": "logo_max_width",
110 | "min": 20,
111 | "max": 420,
112 | "step": 10,
113 | "unit": "px",
114 | "label": "Logo Width",
115 | "default": 60
116 | },
117 | {
118 | "type": "link_list",
119 | "id": "menu",
120 | "label": "Menu",
121 | "default": "main-menu"
122 | }
123 | ]
124 | }
125 | {% endschema %}
126 |
127 | {%- if section.settings.announcement_bar_enabled -%}
128 |
144 | {%- endif -%}
145 |
--------------------------------------------------------------------------------
/src/components/sections/image-with-text/image-with-text.liquid:
--------------------------------------------------------------------------------
1 | {%- comment -%}
2 | This is a required section for the Shopify Theme Store.
3 | It is available when you add the "Image with text" section in the theme editor.
4 |
5 | Theme Store required settings
6 | - Image
7 | - Heading: title of the block
8 | - Text: description of the block
9 |
10 | Theme Store optional settings
11 | - Button label: label associated with the button
12 | - Button link: link of the button
13 | {%- endcomment -%}
14 |
15 |
16 | {% capture image_layout %}
17 |
18 | {% if section.settings.image != blank %}
19 | {% include 'responsive-image' with
20 | image: section.settings.image,
21 | max_width: 545,
22 | max_height: 545
23 | %}
24 | {% else %}
25 | {{ 'image' | placeholder_svg_tag: 'placeholder-svg' }}
26 | {% endif %}
27 |
28 | {% endcapture %}
29 |
30 | {% if section.settings.layout == 'left' %}
31 | {{ image_layout }}
32 | {% endif %}
33 |
34 |
35 | {% if section.settings.title != blank %}
36 |
{{ section.settings.title | escape }}
37 | {% endif %}
38 | {% if section.settings.text != blank %}
39 |
{{ section.settings.text }}
40 | {% endif %}
41 | {% if section.settings.button_label != blank and section.settings.button_link != blank %}
42 |
43 | {{ section.settings.button_label | escape }}
44 |
45 | {% endif %}
46 |
47 |
48 | {% if section.settings.layout == 'right' %}
49 | {{ image_layout }}
50 | {% endif %}
51 |
52 |
53 | {% schema %}
54 | {
55 | "name": "Image with text",
56 | "settings": [
57 | {
58 | "type": "image_picker",
59 | "id": "image",
60 | "label": "Image"
61 | },
62 | {
63 | "type": "select",
64 | "id": "layout",
65 | "label": "Image alignment",
66 | "default": "left",
67 | "options": [
68 | {
69 | "value": "left",
70 | "label": "Left"
71 | },
72 | {
73 | "value": "right",
74 | "label": "Right"
75 | }
76 | ]
77 | },
78 | {
79 | "type": "text",
80 | "id": "title",
81 | "label": "Heading",
82 | "default": "Image with text"
83 | },
84 | {
85 | "type": "richtext",
86 | "id": "text",
87 | "label": "Text",
88 | "default": "
Pair large text with an image to give focus to your chosen product, collection, or blog post. Add details on availability, style, or even provide a review.
"
89 | },
90 | {
91 | "type": "text",
92 | "id": "button_label",
93 | "label": "Button label"
94 | },
95 | {
96 | "type": "url",
97 | "id": "button_link",
98 | "label": "Button link"
99 | }
100 | ],
101 | "presets": [
102 | {
103 | "name": "Image with text",
104 | "category": "Image"
105 | }
106 | ]
107 | }
108 | {% endschema %}
109 |
--------------------------------------------------------------------------------
/src/components/sections/product/product.js:
--------------------------------------------------------------------------------
1 | import {post} from '../../helpers/cart-fetch-api/cart-fetch-api';
2 |
3 | document.getElementById('AddToCartForm').onsubmit = async function (event) {
4 | const btn = document.getElementById('AddToCartBtn');
5 | const id = document.getElementById('AddToCartBtn').value;
6 | const option1 = document.getElementById('option1').value;
7 | const option2 = document.getElementById('option2').value;
8 | const qty = document.getElementById('counterQty').value;
9 |
10 | event.preventDefault();
11 | const response = await post('add.js', {id, option1, option2, qty});
12 |
13 | if(response) {
14 | btn.textContent = 'ITEM ADDED';
15 | }
16 | return false;
17 | };
--------------------------------------------------------------------------------
/src/components/sections/product/product.liquid:
--------------------------------------------------------------------------------
1 | {% assign current_variant = product.selected_or_first_available_variant %}
2 | {% assign featured_image = current_variant.featured_image | default: product.featured_image %}
3 |
4 |
5 |
6 |
9 |
10 | {%- comment -%}
BRAND NAME {%- endcomment -%}
11 |
{{ product.title }}
12 |
{{ product.content }}
13 |
65 |
66 |
67 |
68 |
69 |
70 | {{ 'bundle.product.js' | asset_url | script_tag }}
71 |
--------------------------------------------------------------------------------
/src/components/snippets/dynamic-modal/dynamic-modal.liquid:
--------------------------------------------------------------------------------
1 | {%- comment -%}
2 | This snippet uses Variable Scope and can be re-used throughout the theme.
3 | To use this snippet in sections, add render tag with key: value for assigned variables.
4 | For Example:
5 | This is the target button id that will open the modal.
6 |
TEST MODAL
7 | =========================================================================
8 | Here is a render tag example to use in the section with some example variables declared.
9 | Note the openModalBtn matches the button element above which triggers the modal in this example.
10 | =========================================================================
11 | {%- render 'dynamic-modal',
12 | id: "testModal",
13 | openModalBtn: "modalBtn",
14 | title: "Modal Title",
15 | body: "Modal Body",
16 | buttonOne: "Button One",
17 | buttonOneStyle: "text-white bg-blue-500 hover:bg-blue-700",
18 | buttonOneFunction: "javascript:void(0);",
19 | buttonTwo: "Button Two",
20 | buttonTwoStyle: "text-white bg-transparent hover:bg-blue-700 text-blue-500 hover:text-white border border-blue-500 hover:border-transparent",
21 | buttonTwoFunction: "javascript:void(0);"
22 | -%}
23 | =========================================================================
24 | id, openModalBtn, title and body are required. If you don't want buttons make the value = null.
25 | =========================================================================
26 | {%- endcomment -%}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
{{ title }}
34 |
35 |
37 |
39 |
40 |
41 |
42 |
43 |
{{ body }}
44 |
45 |
46 | {%- if buttonOne -%}
47 |
50 | {{ buttonOne }}
51 |
52 | {%- endif -%}
53 | {%- if buttonTwo -%}
54 |
57 | {{ buttonTwo }}
58 |
59 | {%- endif -%}
60 |
61 |
62 |
63 |
64 |
65 |
66 |
90 |
--------------------------------------------------------------------------------
/src/components/snippets/global-css/global-css.js:
--------------------------------------------------------------------------------
1 | import './global-css.scss';
--------------------------------------------------------------------------------
/src/components/snippets/global-css/global-css.liquid:
--------------------------------------------------------------------------------
1 | {%- comment -%}Declare liquid variables here and use then in the global-css.scss file{%- endcomment -%}
2 | {% style %}
3 | {{ settings.font_heading | font_face }}
4 | {{ settings.font_body | font_face }}
5 |
6 | {%- assign font_body_bold = settings.font_body | font_modify: 'weight', 'bolder' -%}
7 | {%- assign font_body_bold_italic = font_body_bold | font_modify: 'style', 'italic' -%}
8 |
9 | {{ font_body_bold | font_face }}
10 | {{ font_body_bold_italic | font_face }}
11 |
12 | :root {
13 | --font-heading: {{ settings.font_heading.family }}, {{ settings.font_heading.fallback_families }};
14 | --font-body: {{ settings.font_body.family }}, {{ settings.font_body.fallback_families }};
15 | --font-body-weight: {{ settings.font_body.weight }};
16 | --font-body-style: {{ settings.font_body.style }};
17 | --font-body-bold-weight: {{ font_body_bold.weight | default: 'bold' }};
18 | }
19 | {% endstyle %}
20 |
--------------------------------------------------------------------------------
/src/components/snippets/global-css/global-css.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Typography
3 | $font-heading: var(--font-heading);
4 | $font-body: var(--font-body);
5 | $font-heading-base-size: var(--font-text-base-size);
6 | $font-body-weight: var(--font-body-weight);
7 | $font-body-style: var(--font-body-style);
8 | $font-body-bold-weight: var(--font-body-bold-weight);
9 |
10 | html {
11 | font-size: 16px;
12 | }
13 |
14 | body {
15 | font-family: $font-body;
16 | font-weight: $font-body-weight;
17 | font-style: $font-body-style;
18 | }
19 |
20 | h1,
21 | h2,
22 | h3,
23 | h4,
24 | h5,
25 | h6 {
26 | font-family: $font-heading;
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/src/components/snippets/input-counter/input-counter.js:
--------------------------------------------------------------------------------
1 | import './input-counter.scss';
2 |
3 | function decrement(el) {
4 | const btn = el.target.parentNode.parentElement.querySelector('button[data-action="decrement"]');
5 | const target = btn.nextElementSibling;
6 | let value = Number(target.value);
7 | if (value > 1) {
8 | value--;
9 | target.value = value;
10 | }
11 | }
12 |
13 | function increment(el) {
14 | const btn = el.target.parentNode.parentElement.querySelector('button[data-action="decrement"]');
15 | const target = btn.nextElementSibling;
16 | let value = Number(target.value);
17 | value++;
18 | target.value = value;
19 | }
20 |
21 | const decrementButtons = document.querySelectorAll('button[data-action="decrement"]');
22 | const incrementButtons = document.querySelectorAll('button[data-action="increment"]');
23 | decrementButtons.forEach(btn => {
24 | btn.addEventListener('click', decrement);
25 | });
26 | incrementButtons.forEach(btn => {
27 | btn.addEventListener('click', increment);
28 | });
--------------------------------------------------------------------------------
/src/components/snippets/input-counter/input-counter.liquid:
--------------------------------------------------------------------------------
1 | {{ 'bundle.input-counter.css' | asset_url | stylesheet_tag }}
2 |
3 |
20 |
21 | {{ 'bundle.input-counter.js' | asset_url | script_tag }}
22 |
--------------------------------------------------------------------------------
/src/components/snippets/input-counter/input-counter.scss:
--------------------------------------------------------------------------------
1 | input[type='number']::-webkit-inner-spin-button,
2 | input[type='number']::-webkit-outer-spin-button {
3 | -webkit-appearance: none;
4 | margin: 0;
5 | }
6 |
7 | .custom-number-input input:focus {
8 | outline: none !important;
9 | }
10 |
11 | .custom-number-input button:focus {
12 | outline: none !important;
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/snippets/message/message.js:
--------------------------------------------------------------------------------
1 | // testing importing scss
2 | import './message.scss';
3 |
4 | // Testing arrow functions
5 | const messageFunction = () => 'Message JS ES6 Function';
6 | console.log(messageFunction());
7 |
--------------------------------------------------------------------------------
/src/components/snippets/message/message.liquid:
--------------------------------------------------------------------------------
1 | {{ 'bundle.message.css' | asset_url | stylesheet_tag }}
2 |
3 |
4 |
This is a message from a snippet
5 |
6 |
7 | {{ 'bundle.message.js' | asset_url | script_tag }}
8 |
--------------------------------------------------------------------------------
/src/components/snippets/message/message.scss:
--------------------------------------------------------------------------------
1 | .message_container {
2 | display: flex;
3 | color: #000;
4 | }
5 | .message_heading {
6 | color: purple;
7 | font-size: 2rem;
8 | text-align: right;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/snippets/pagination/accessible-pagination.liquid:
--------------------------------------------------------------------------------
1 | {%- if paginate.pages > 1 -%}
2 |
3 |
4 | {%- if paginate.previous-%}
5 |
6 | Previous
7 |
8 | {%- else -%}
9 | Previous
10 | {%- endif -%}
11 |
12 | {%- for part in paginate.parts -%}
13 | {%- if part.is_link -%}
14 |
15 | {{ part.title }}
16 |
17 | {%- else -%}
18 | {%- if part.title == paginate.current_page -%}
19 | {{ part.title }}
20 | {%- else -%}
21 | {{ part.title }}
22 | {%- endif -%}
23 | {%- endif -%}
24 | {%- endfor -%}
25 |
26 | {%- if paginate.next -%}
27 | Next
28 | {%- else -%}
29 | Next
30 | {%- endif -%}
31 |
32 |
33 | {%- endif -%}
34 |
--------------------------------------------------------------------------------
/src/components/snippets/pagination/default-pagination.liquid:
--------------------------------------------------------------------------------
1 |
2 | {{ paginate | default_pagination: next: 'Older', previous: 'Newer' }}
3 |
--------------------------------------------------------------------------------
/src/components/snippets/responsive-bg-image/responsive-bg-image.liquid:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | It creates a style tag containing "background-image" properties to load the correct image depending on the resolution
3 | Dependencies:
4 | - Lazysizes plugin (https://github.com/aFarkas/lazysizes) which enable responsive image with faster load time and better performance.
5 | - Lazysizes Bgset (https://github.com/aFarkas/lazysizes/tree/gh-pages/plugins/bgset) To use responsive images on background-image (CSS)
6 |
7 | Accepts:
8 | - image: {Object} Image object
9 |
10 | Usage:
11 | In your liquid template file, copy the following line
12 |
13 |
14 | {% endcomment %}
15 | {%- if image != blank -%}
16 | {%- if image.width > 180 -%}{{ image | img_url: '180x' }} 180w {{ 180 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
17 | {%- if image.width > 360 -%}{{ image | img_url: '360x' }} 360w {{ 360 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
18 | {%- if image.width > 540 -%}{{ image | img_url: '540x' }} 540w {{ 540 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
19 | {%- if image.width > 720 -%}{{ image | img_url: '720x' }} 720w {{ 720 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
20 | {%- if image.width > 900 -%}{{ image | img_url: '900x' }} 900w {{ 900 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
21 | {%- if image.width > 1080 -%}{{ image | img_url: '1080x' }} 1080w {{ 1080 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
22 | {%- if image.width > 1296 -%}{{ image | img_url: '1296x' }} 1296w {{ 1296 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
23 | {%- if image.width > 1512 -%}{{ image | img_url: '1512x' }} 1512w {{ 1512 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
24 | {%- if image.width > 1728 -%}{{ image | img_url: '1728x' }} 1728w {{ 1728 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
25 | {%- if image.width > 1944 -%}{{ image | img_url: '1944x' }} 1944w {{ 1944 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
26 | {%- if image.width > 2160 -%}{{ image | img_url: '2160x' }} 2160w {{ 2160 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
27 | {%- if image.width > 2376 -%}{{ image | img_url: '2376x' }} 2376w {{ 2376 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
28 | {%- if image.width > 2592 -%}{{ image | img_url: '2592x' }} 2592w {{ 2592 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
29 | {%- if image.width > 2808 -%}{{ image | img_url: '2808x' }} 2808w {{ 2808 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
30 | {%- if image.width > 3024 -%}{{ image | img_url: '3024x' }} 3024w {{ 3024 | divided_by: image.aspect_ratio | round }}h,{%- endif -%}
31 | {%- assign image_size = image.width | append: 'x' -%}
32 | {{ image | img_url: image_size }} {{ image.width }}w {{ image.height }}h
33 | {%- endif -%}
34 |
--------------------------------------------------------------------------------
/src/components/snippets/responsive-image/responsive-image.liquid:
--------------------------------------------------------------------------------
1 | {%- comment -%}
2 | It creates a style tag and it restricts an image from growing larger than its max resolution.
3 |
4 | Dependencies:
5 | - Lazysizes plugin (https://github.com/aFarkas/lazysizes) which enable responsive image with faster load time and better performance.
6 | - Lazysizes Responsive Images as a Service (https://github.com/aFarkas/lazysizes/tree/gh-pages/plugins/rias) To load the correct image size with our CDN
7 | - Lazysizes Bgset (https://github.com/aFarkas/lazysizes/tree/gh-pages/plugins/bgset) To use responsive images on background-image (CSS)
8 |
9 | Accepts:
10 | - max_width: {Number} Max width of the image container
11 | - max_height: {Number} Max height of the image container
12 | - image: {Object} Image object
13 | - image_class: {String} class name of the
14 | - image_attributes: {String} additional HTML attributes of the
15 | - wrapper_class: {String} class name of the
wrapper
16 | - wrapper_attributes: {String} additional HTML attributes of the
wrapper
17 |
18 | Usage:
19 | In your liquid template file, copy the following line
20 | - {% include 'responsive-image' with image: featured_image, image_class: "css-class", wrapper_class: "wrapper-css-class", max_width: 700, max_height: 800 %}
21 | {%- endcomment -%}
22 |
23 | {%- comment -%} Added incremental number to avoid conflict styling code when the same images are using this snippet {%- endcomment -%}
24 | {%- capture responsive_image_counter %}{% increment responsive_image_counter %}{% endcapture -%}
25 |
26 |
57 |
58 | {%- assign img_url = image | img_url: '1x1' | replace: '_1x1.', '_{width}x.' -%}
59 |
60 | {%- comment -%} Limit image widths to valid options based on image.width {%- endcomment -%}
61 | {%- assign image_widths = '180,360,540,720,900,1080,1296,1512,1728,1944,2160,2376,2592,2808,3024' | split: ',' -%}
62 | {%- capture image_widths -%}
63 | {%- for width in image_widths -%}
64 | {%- comment -%} Check if image width is less or equal to width {%- endcomment -%}
65 | {%- assign width_num = width | plus: 0 | round -%}
66 | {%- if image.width >= width_num -%}{{ width_num }},{%- endif -%}
67 | {%- endfor -%}
68 | {{ image.width }}
69 | {%- endcapture -%}
70 | {%- assign image_widths = image_widths | strip -%}
71 |
72 |
73 |
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/src/components/tailwind.css:
--------------------------------------------------------------------------------
1 | /* purgecss start ignore */
2 | @tailwind base;
3 | @tailwind components;
4 | /* purgecss end ignore */
5 | @tailwind utilities;
6 |
7 | /* Add Custom CSS Below this line */
8 |
--------------------------------------------------------------------------------
/src/components/templates/404.liquid:
--------------------------------------------------------------------------------
1 |
{{ 'general.404.title' | t }}
2 |
{{ 'general.404.subtext_html' | t }}
3 |
--------------------------------------------------------------------------------
/src/components/templates/article.liquid:
--------------------------------------------------------------------------------
1 | {% assign number_of_comments = article.comments_count %}
2 | {% if comment and comment.created_at %}
3 | {% assign number_of_comments = article.comments_count %}
4 | {% endif %}
5 |
6 |
{{ article.title }}
7 | {% capture author %}
{{ article.author }} {% endcapture %}
8 | {% capture date %}
{{ article.published_at | date: format: 'month_day_year' }} {% endcapture %}
9 | {{ article.author }} @ {{ article.created_at }}
10 |
11 |
{{ article.content }}
12 | {% if blog.comments_enabled? %}
13 |
{{ number_of_comments }} comments
14 | {% paginate article.comments by 5 %}
15 | {% for comment in article.comments %}
16 |
17 |
{{ comment.content }}
18 | {{ comment.author }} @ {{ comment.created_at }}
19 |
20 | {% endfor %}
21 | {% if paginate.pages > 1 %}
22 | {{ paginate | default_pagination }}
23 | {% endif %}
24 | {% endpaginate %}
25 |
26 |
27 | {% form 'new_comment', article %}
28 | {{ form.errors | default_errors }}
29 | name
30 |
31 |
32 | email
33 |
34 |
35 | message
36 |
37 |
38 |
39 | {% endform %}
40 |
41 | {% endif %}
42 |
--------------------------------------------------------------------------------
/src/components/templates/blog.liquid:
--------------------------------------------------------------------------------
1 | {% paginate blog.articles by 5 %}
2 |
3 |
{{ blog.title }}
4 | {% for article in blog.articles %}
5 |
6 |
7 | {{ article.author }} @ {{ article.created_at }}
8 |
9 | {% if article.excerpt.size > 0 %}
10 | {{ article.excerpt }}
11 | {% else %}
12 |
{{ article.content | strip_html | truncatewords: 100 }}
13 | {% endif %}
14 |
15 |
16 | {% endfor %}
17 |
18 | {% if paginate.pages > 1 %}
19 | {{ paginate | default_pagination }}
20 | {% endif %}
21 |
22 | {% endpaginate %}
23 |
--------------------------------------------------------------------------------
/src/components/templates/cart.liquid:
--------------------------------------------------------------------------------
1 | {% if cart.item_count > 0 %}
2 |
Your Cart
3 | {% section 'cart' %}
4 | {% else %}
5 |
cart
6 | Cart is empty
7 | {% endif %}
--------------------------------------------------------------------------------
/src/components/templates/collection.liquid:
--------------------------------------------------------------------------------
1 |
2 | {% paginate collection.products by 8 %}
3 | {{ collection.title }}
4 |
26 | {%- if settings.pagination and paginate.pages > 1 -%}
27 | {%- comment -%}Pagination Control in Theme Settings{%- endcomment -%}
28 | {%- if settings.pagination_type == 'accessable_pagination' -%}
29 | {%- render 'accessible-pagination' with { paginate: paginate} -%}
30 | {%- elsif settings.pagination_type == 'default_pagination' -%}
31 | {%- render 'default-pagination' with { paginate: paginate} -%}
32 | {%- endif -%}
33 | {%- endif -%}
34 | {% endpaginate %}
35 |
36 |
--------------------------------------------------------------------------------
/src/components/templates/customers/account.liquid:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/templates/customers/activate_account.liquid:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/templates/customers/addresses.liquid:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/templates/customers/login.liquid:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/templates/customers/order.liquid:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/templates/customers/register.liquid:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/templates/customers/reset_password.liquid:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/templates/gift_card.liquid:
--------------------------------------------------------------------------------
1 | {% layout none %}
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ 'vendor/qrcode.js' | shopify_asset_url | script_tag }}
9 |
10 |
11 |
12 |
19 |
20 | {% if gift_card.pass_url %}
21 |
22 |
23 |
24 | {% endif %}
25 |
26 |
27 |