diff --git a/storefront-reference-architecture/.circleci/config.yml b/storefront-reference-architecture/.circleci/config.yml new file mode 100644 index 0000000..7bc7b87 --- /dev/null +++ b/storefront-reference-architecture/.circleci/config.yml @@ -0,0 +1,17 @@ +orbs: + node: circleci/node@1.1 + +jobs: + build: + working_directory: ~/build_only + executor: + name: node/default + tag: '12.21' + steps: + - checkout + - run: npm install + - run: npm run lint + - run: npm run test + - run: npm run compile:js + - run: npm run compile:scss +version: 2.1 diff --git a/storefront-reference-architecture/.editorconfig b/storefront-reference-architecture/.editorconfig new file mode 100644 index 0000000..d8c89d2 --- /dev/null +++ b/storefront-reference-architecture/.editorconfig @@ -0,0 +1,14 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[package.json] +indent_style = space +indent_size = 2 diff --git a/storefront-reference-architecture/.eslintignore b/storefront-reference-architecture/.eslintignore new file mode 100644 index 0000000..28bda13 --- /dev/null +++ b/storefront-reference-architecture/.eslintignore @@ -0,0 +1,6 @@ +cartridges/app_storefront_base/cartridge/static/ +coverage/ +doc/ +bin/ +codecept.conf.js +cartridges/bm_app_storefront_base/cartridge/static/ \ No newline at end of file diff --git a/storefront-reference-architecture/.eslintrc.json b/storefront-reference-architecture/.eslintrc.json new file mode 100644 index 0000000..d08e943 --- /dev/null +++ b/storefront-reference-architecture/.eslintrc.json @@ -0,0 +1,15 @@ +{ + "root": true, + "extends": "airbnb-base/legacy", + "rules": { + "import/no-unresolved": "off", + "indent": ["error", 4, { "SwitchCase": 1, "VariableDeclarator": 1 }], + "func-names": "off", + "require-jsdoc": "error", + "valid-jsdoc": ["error", { "preferType": { "Boolean": "boolean", "Number": "number", "object": "Object", "String": "string" }, "requireReturn": false}], + "vars-on-top": "off", + "global-require": "off", + "no-shadow": ["error", { "allow": ["err", "callback"]}], + "max-len": "off" + } +} diff --git a/storefront-reference-architecture/.gitignore b/storefront-reference-architecture/.gitignore new file mode 100644 index 0000000..d1b387b --- /dev/null +++ b/storefront-reference-architecture/.gitignore @@ -0,0 +1,43 @@ +node_modules/ +cartridges/app_storefront_base/cartridge/static/default/css/ +cartridges/app_storefront_base/cartridge/static/default/js/ +cartridges/app_storefront_base/cartridge/static/default/fonts/ + +cartridges/app_storefront_base/cartridge/static/fr_FR/css/ +cartridges/app_storefront_base/cartridge/static/fr_FR/js/ +cartridges/app_storefront_base/cartridge/static/fr_FR/fonts/ + +cartridges/app_storefront_base/cartridge/static/it_IT/css/ +cartridges/app_storefront_base/cartridge/static/it_IT/js/ +cartridges/app_storefront_base/cartridge/static/it_IT/fonts/ + +cartridges/app_storefront_base/cartridge/static/ja_JP/css/ +cartridges/app_storefront_base/cartridge/static/ja_JP/js/ +cartridges/app_storefront_base/cartridge/static/ja_JP/fonts/ + +cartridges/app_storefront_base/cartridge/static/zh_CN/css/ +cartridges/app_storefront_base/cartridge/static/zh_CN/js/ +cartridges/app_storefront_base/cartridge/static/zh_CN/fonts/ + +cartridges/app_storefront_base/cartridge/static/en_GB/css/ +cartridges/app_storefront_base/cartridge/static/en_GB/js/ +cartridges/app_storefront_base/cartridge/static/en_GB/fonts/ + +coverage/ +npm-debug.log +cartridges.zip +.idea/ +dw.json +sitegenesisdata/ +mobilefirstdata/ +storefrontdata/ +demo_data_sfra/ +demo_data_sfra.zip +.DS_Store +test/appium/webdriver/config.json +.vscode +.history +*.iml +.idea +test/acceptance/report +test/integration/config.json \ No newline at end of file diff --git a/storefront-reference-architecture/.istanbul.yml b/storefront-reference-architecture/.istanbul.yml new file mode 100644 index 0000000..edd7d10 --- /dev/null +++ b/storefront-reference-architecture/.istanbul.yml @@ -0,0 +1,16 @@ +instrumentation: + root: . + extensions: + - .js + default-excludes: true + excludes: [ + "**/static/**", # Those are pre-processed client-side scripts + "**/js/**", # Those are client-side scripts + "**/controllers/**", # We can't test controllers without too much mocking + "**/server/EventEmitter.js", # Third-party library + "**/modules/*.js", # Those are just wrappers around modules + "bin/*", # Those are task files + "**/scripts/payment/processor/*", # Those are payment processor files, we don't test them + "webpack.config.js" # This is webpack config for javascript + ] + include-all-sources: true diff --git a/storefront-reference-architecture/.stylelintrc.json b/storefront-reference-architecture/.stylelintrc.json new file mode 100644 index 0000000..81f9672 --- /dev/null +++ b/storefront-reference-architecture/.stylelintrc.json @@ -0,0 +1,17 @@ +{ + "extends": "stylelint-config-standard", + "plugins": [ + "stylelint-scss" + ], + "rules": { + "at-rule-no-unknown": [true, { "ignoreAtRules": ["include", "each", "mixin", "if", "else", "content"] } ], + "indentation": 4, + "scss/at-import-no-partial-leading-underscore": true, + "scss/at-import-partial-extension-blacklist": ["scss"], + "scss/dollar-variable-no-missing-interpolation": true, + "scss/media-feature-value-dollar-variable": "always", + "scss/selector-no-redundant-nesting-selector": true, + "at-rule-empty-line-before": [ "always", { "ignoreAtRules": ["else"], "ignore": ["blockless-after-same-name-blockless", "inside-block"] } ], + "block-closing-brace-newline-after": [ "always", { "ignoreAtRules": ["if", "else"] } ] + } +} diff --git a/storefront-reference-architecture/CONTRIBUTING.md b/storefront-reference-architecture/CONTRIBUTING.md new file mode 100644 index 0000000..92860a5 --- /dev/null +++ b/storefront-reference-architecture/CONTRIBUTING.md @@ -0,0 +1,106 @@ +# Table of contents + +- [Conventions for branch names and commit messages ](#conventions-for-branch-names-and-commit-messages) +- [Submitting your first pull request ](#submitting-your-first-pull-request) +- [Submitting a pull request ](#submitting-a-pull-request) +- [What to expect](#what-to-expect) +- [Community contributors](#community-contributors) +- [Contributor License Agreement (CLA)](#contributor-license-agreement) +- [Commit signing](#commit-signing) +- [Back to README](./README.md) + +# Contributing to SFRA + +To contribute to the SFRA base cartridge, follow the guidelines below. This helps us address your pull request in a more timely manner. + +## Conventions for branch names and commit messages + +### Branch names + +To name a branch, use the following pattern: `yourusername-description` + +In this pattern, `description` is dash-delimited. + +For example: jdoe-unify-shipping-isml + +### Commit messages + +To create a commit message, use the following pattern: `action-term: short-description` + +In this pattern, `action-term` is one of the following: + +* Bug +* Doc +* Chore +* Update +* Breaking +* New + +After `action-term,` add a colon, and then write a short description. You can optionally include a GUS ticket number in parentheses. + +For example: "Breaking: Unify the single- and multi-ship shipping isml templates (W-999999)." + +## Submitting your first pull request +If this is your first pull request, follow these steps: + + 1. Create a fork of the SFRA repository + + 2. Download the forked repository + + 3. Checkout the integration branch + + 4. Apply your code fix + + 5. Create a pull request against the integration branch + +## Submitting a pull request + + 1. Create a branch off the integration branch. + * To reduce merge conflicts, rebase your branch before submitting your pull request. + * If applicable, reference the issue number in the comments of your pull request. + + 2. In your pull request, include: + * A brief description of the problem and your solution + * (optional) Screen shots + * (optional) Error logs + * (optional) Steps to reproduce + + 3. Grant SFRA team members access to your fork so we can run an automated test on your pull request prior to merging it into our integration branch. + * From within your forked repository, find the 'Settings' link (see the site navigation on left of the page). + * Under the settings menu, click 'User and group access'. + * Add the new user to the input field under the heading 'Users' and give the new user write access. + + 4. Indicate if there is any data that needs to be included with your code submission. + + 5. Your code should pass the automation process. + * Lint your code: + `npm run lint` + * Run and pass the unit test: + `npm run test` + * Run and pass the unit/intergration test: + `npm run test:integration` + +## What to expect + +After you submit your pull request, we'll look it over and consider it for merging. + +As long as your submission has met the above guidelines, we should merge it in a timely manner. + +Our sprints run for about two weeks; in that period of time, we typically review all pull requests, give feedback, and merge the request (depending on our current sprint priorities). + +## Community contributors + +To speed up the process of reviewing and merging your pull request, grant the following team members access to your fork: + + * SFRA team + +## Contributor License Agreement + +All external contributors must sign our Contributor License Agreement (CLA). + +## Commit signing + +All contributors must set up [commit signing](https://help.github.com/en/github/authenticating-to-github/signing-commits). + + + diff --git a/storefront-reference-architecture/PULL_REQUEST_TEMPLATE.md b/storefront-reference-architecture/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..7b8babf --- /dev/null +++ b/storefront-reference-architecture/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,34 @@ +## Applicable to all contributors to this repository: + +### All pull requests: +* [ ] Have you considered security (e.g., XSS)? +* [ ] Have you considered desktop, mobile, and tablet form-factors? +* [ ] Have you considered [accessibility best practices](https://www.w3.org/WAI/standards-guidelines/wcag/)? + +### Code +* [ ] Are the commits squashed into one commit? +* [ ] Is the [branch name and commit message](CONTRIBUTING.md#conventions-for-branch-names-and-commit-messages) following team convention? +* [ ] Is the code linted? +* [ ] Are all open issues, questions, and concerns by reviewers answered, resolved, and reviewed? + +### Quality assurance +* [ ] If applicable, are unit tests written and are they passing? +* [ ] If applicable, are integration tests written and are they passing? +* [ ] Have checks linked in your pull request passed? + +### Documentation +* [ ] Are there meaningful code comments included? + +## Applicable to community contributors: +* [ ] Have you granted SFRA team access to your fork? [Grant access](CONTRIBUTING.md#community-contributors) + +## Applicable to SFRA team members: + +### Documentation +* [ ] If applicable, is the change log updated? +* [ ] If applicable, have any UI text changes been reviewed by Documentation? +* [ ] If applicable, have any UI implementations been reviewed by the UX team? + +### Security +* [ ] If applicable, has Security reviewed this code? +* [ ] If applicable, is a 3PP request submitted? diff --git a/storefront-reference-architecture/README.md b/storefront-reference-architecture/README.md new file mode 100644 index 0000000..e76242b --- /dev/null +++ b/storefront-reference-architecture/README.md @@ -0,0 +1,101 @@ +# Storefront Reference Architecture (SFRA) + +This is a repository for the Storefront Reference Architecture reference application. + +Storefront Reference Architecture has a base cartridge (`app_storefront_base`) provided by Commerce Cloud that is never directly customized or edited. Instead, customization cartridges are layered on top of the base cartridge. This change is intended to allow for easier adoption of new features and bug fixes. +Storefront Reference Architecture supplies an [plugin_applepay](https://github.com/SalesforceCommerceCloud/plugin-applepay) plugin cartridge to demonstrate how to layer customizations for the reference application. + +Your feedback on the ease-of-use and limitations of this new architecture is invaluable during the developer preview. Particularly, feedback on any issues you encounter or workarounds you develop for efficiently customizing the base cartridge without editing it directly. + + +# The latest version + +The latest version of SFRA is 6.0.0 + +# Getting Started + +1. Clone this repository. + +2. Run `npm install` to install all of the local dependencies (SFRA has been tested with v12.21.0 and is recommended) + +3. Run `npm run compile:js` from the command line that would compile all client-side JS files. Run `npm run compile:scss` and `npm run compile:fonts` that would do the same for css and fonts. + +4. Create `dw.json` file in the root of the project: +```json +{ + "hostname": "your-sandbox-hostname.demandware.net", + "username": "yourlogin", + "password": "yourpwd", + "code-version": "version_to_upload_to" +} +``` + +5. Run `npm run uploadCartridge`. It will upload `app_storefront_base`, `modules` and `bm_app_storefront_base` cartridges to the sandbox you specified in `dw.json` file. + +6. Use https://github.com/SalesforceCommerceCloud/storefrontdata to zip and import site data on your sandbox. + +7. Add the `app_storefront_base` cartridge to your cartridge path in _Administration > Sites > Manage Sites > RefArch - Settings_ (Note: This should already be populated by the sample data in Step 6). + +8. You should now be ready to navigate to and use your site. + +# NPM scripts +Use the provided NPM scripts to compile and upload changes to your Sandbox. + +## Compiling your application + +* `npm run compile:scss` - Compiles all .scss files into CSS. +* `npm run compile:js` - Compiles all .js files and aggregates them. +* `npm run compile:fonts` - Copies all needed font files. Usually, this only has to be run once. + + If you are having an issue compiling scss files, try running 'npm rebuild node-sass' from within your local repo. + +## Linting your code + +`npm run lint` - Execute linting for all JavaScript and SCSS files in the project. You should run this command before committing your code. + +## Watching for changes and uploading + +`npm run watch` - Watches everything and recompiles (if necessary) and uploads to the sandbox. Requires a valid `dw.json` file at the root that is configured for the sandbox to upload. + +## Uploading + +`npm run uploadCartridge` - Will upload `app_storefront_base`, `modules` and `bm_app_storefront_base` to the server. Requires a valid `dw.json` file at the root that is configured for the sandbox to upload. + +`npm run upload ` - Will upload a given file to the server. Requires a valid `dw.json` file. + +# Testing +## Running unit tests + +You can run `npm test` to execute all unit tests in the project. Run `npm run cover` to get coverage information. Coverage will be available in `coverage` folder under root directory. + +* UNIT test code coverage: +1. Open a terminal and navigate to the root directory of the mfsg repository. +2. Enter the command: `npm run cover`. +3. Examine the report that is generated. For example: `Writing coverage reports at [/Users/yourusername/SCC/sfra/coverage]` +3. Navigate to this directory on your local machine, open up the index.html file. This file contains a detailed report. + +## Running integration tests +Integration tests are located in the `storefront-reference-architecture/test/integration` directory. + +To run integration tests you can use the following command: + +``` +npm run test:integration +``` + +**Note:** Please note that short form of this command will try to locate URL of your sandbox by reading `dw.json` file in the root directory of your project. If you don't have `dw.json` file, integration tests will fail. +sample `dw.json` file (this file needs to be in the root of your project) +{ + "hostname": "devxx-sitegenesis-dw.demandware.net" +} + +You can also supply URL of the sandbox on the command line: + +``` +npm run test:integration -- --baseUrl devxx-sitegenesis-dw.demandware.net +``` + +# [Contributing to SFRA](./CONTRIBUTING.md) + +#Page Designer Components for Storefront Reference Architecture +See: [Page Designer Components](./page-designer-components.md) diff --git a/storefront-reference-architecture/bin/Makefile.js b/storefront-reference-architecture/bin/Makefile.js new file mode 100644 index 0000000..e5deec2 --- /dev/null +++ b/storefront-reference-architecture/bin/Makefile.js @@ -0,0 +1,140 @@ +'use strict'; + +/* global cat, cd, cp, echo, exec, exit, find, ls, mkdir, pwd, rm, target, test */ + +require('shelljs/make'); + +var chalk = require('chalk'), + path = require('path'), + spawn = require('child_process').spawn, + fs = require('fs'), + shell = require('shelljs'); + +function getSandboxUrl() { + if (test('-f', path.join(process.cwd(), 'dw.json'))) { + var config = cat(path.join(process.cwd(), 'dw.json')); + var parsedConfig = JSON.parse(config); + return '' + parsedConfig.hostname; + } + return ''; +} + +function getOptions(defaults, args) { + var params = {}; + var i = 0; + while (i < args.length) { + var item = args[i]; + if (item.indexOf('--') === 0) { + if (i + 1 < args.length && args[i + 1].indexOf('--') < 0) { + var value = args[i + 1]; + value = value.replace(/\/+$/, ""); + params[item.substr(2)] = value; + i += 2; + } else { + params[item.substr(2)] = true; + i++; + } + } else { + params[item] = true; + i++; + } + } + var options = Object.assign({}, defaults, params); + return options; +} + +function getOptionsString(options) { + if (!options.baseUrl) { + console.error(chalk.red('Could not find baseUrl parameter.')); + process.exit(); + } + + var optionsString = ''; + + Object.keys(options).forEach(function (key) { + if (options[key] === true) { + optionsString += key + ' '; + } else { + optionsString += '--' + key + ' ' + options[key] + ' '; + } + }); + + return optionsString; +} + +target.compileFonts = function () { + var fontsDir = 'cartridges/app_storefront_base/cartridge/static/default/fonts'; + mkdir('-p', fontsDir); + cp('-r', 'node_modules/font-awesome/fonts/', 'cartridges/app_storefront_base/cartridge/static/default'); + cp('-r', 'node_modules/flag-icon-css/flags', fontsDir + '/flags'); +}; + +target.functional = function (args) { + var defaults = { + baseUrl: 'https://' + getSandboxUrl() + '/s/RefArch', + client: 'chrome' + }; + + var configFile = 'test/functional/webdriver/wdio.conf.js'; + if(args.indexOf('appium') > -1) { + args.splice(args.indexOf('appium'), 1); + configFile = 'test/functional/webdriver/wdio.appium.js'; + defaults = { + baseUrl: 'https://' + getSandboxUrl() + '/s/RefArch' + } + } + + var options = getOptions(defaults, args); + var optionsString = getOptionsString(options); + + console.log(chalk.green('Installing selenium')); + exec('node_modules/.bin/selenium-standalone install', { silent: true }); + + console.log(chalk.green('Selenium Server started')); + var selenium = exec('node_modules/.bin/selenium-standalone start', { async: true, silent: true }); + + console.log(chalk.green('Running functional tests')); + + var tests = spawn('./node_modules/.bin/wdio ' + configFile + ' ' + optionsString, { stdio: 'inherit', shell: true }); + + tests.on('exit', function (code) { + selenium.kill(); + console.log(chalk.green('Stopping Selenium Server')); + process.exit(code); + }); +}; + +target.release = function (args) { + if (!args) { + console.log('No version type provided. Please specify release type patch/minor/major'); + return; + } + var type = args[0].replace(/"/g, ''); + if (['patch', 'minor', 'major'].indexOf(type) >= 0) { + console.log('Updating package.json version with ' + args[0] + ' release.'); + var version = spawn('npm version ' + args[0], { stdio: 'inherit', shell: true }); + var propertiesFileName = path.resolve('./cartridges/app_storefront_base/cartridge/templates/resources/version.properties') + + version.on('exit', function (code) { + if (code === 0) { + var versionNumber = JSON.parse(fs.readFileSync('./package.json').toString()).version; + //modify version.properties file + var propertiesFile = fs.readFileSync(propertiesFileName).toString(); + var propertiesLines = propertiesFile.split('\n'); + var newLines = propertiesLines.map(function (line) { + if (line.indexOf('global.version.number=') === 0) { + line = 'global.version.number=' + versionNumber; + } + return line; + }); + fs.writeFileSync(propertiesFileName, newLines.join('\n')); + shell.exec('git add -A'); + shell.exec('git commit -m "Release ' + versionNumber + '"'); + console.log('Version updated to ' + versionNumber); + console.log('Please do not forget to push your changes to the integration branch'); + } + }); + } else { + console.log('Could not release new version. Please specify version type (patch/minor/major).'); + } +} diff --git a/storefront-reference-architecture/bin/test-functional-docker.sh b/storefront-reference-architecture/bin/test-functional-docker.sh new file mode 100755 index 0000000..886f415 --- /dev/null +++ b/storefront-reference-architecture/bin/test-functional-docker.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +HUB_RUNNING=$(docker inspect --format="{{ .State.Running }}" selenium-hub 2> /dev/null) + +if [ $? -eq 1 ]; then + echo "Selenium Hub does not exist. Attempting to run it..." + docker run -d -p 4444:4444 --name selenium-hub --restart always selenium/hub:2.53.0 + sleep 2 +fi + +if [ "$HUB_RUNNING" == "false" ]; then + echo "Selenium Hub is not running. Attempting to start it..." + docker start selenium-hub + sleep 2 +fi + + +CONTAINER_NAME="sg-functional-test-chrome-$JOB_NAME" +# remove any pre-existing container +if [ $(docker ps -a | grep $CONTAINER_NAME | awk '{print $NF}' | wc -l) -gt 0 ]; then + docker rm -f $CONTAINER_NAME 1>/dev/null +fi + +# if a debug flag is passed in, use the debug image and open vnc screen sharing +if [[ $@ == *"--debug"* ]]; then + ip=$(grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' <<< "$@") + docker run -d --link selenium-hub:hub --name $CONTAINER_NAME -p 5900:5900 -v /dev/shm:/dev/shm selenium/node-chrome-debug:2.53.0 1>/dev/null + sleep 2 # wait a bit for container to start + open vnc://:secret@"$ip":5900 +else + docker run -d --link selenium-hub:hub --name $CONTAINER_NAME -v /dev/shm:/dev/shm selenium/node-chrome:2.53.0 1>/dev/null + sleep 2 +fi + +# run actual test command in a subshell to be able to rm docker container afterwards +# this command is for Jenkins job to by pass the Makefile.js +( + ./node_modules/.bin/wdio test/functional/webdriver/wdio.conf.js "$@" 2> /dev/null +) + +# save exit code of subshell +testresult=$? + +docker stop $CONTAINER_NAME 1>/dev/null && docker rm $CONTAINER_NAME 1>/dev/null + +exit $testresult diff --git a/storefront-reference-architecture/bitbucket-pipelines.yml b/storefront-reference-architecture/bitbucket-pipelines.yml new file mode 100644 index 0000000..a54f8ba --- /dev/null +++ b/storefront-reference-architecture/bitbucket-pipelines.yml @@ -0,0 +1,20 @@ +# This is a sample build configuration for Javascript. +# Check our guides at https://confluence.atlassian.com/x/VYk8Lw for more examples. +# Only use spaces to indent your .yml configuration. +# ----- +# You can specify a custom docker image from Docker Hub as your build environment. +image: node:6.9.2 + +pipelines: + default: + - step: + script: # Modify the commands below to build your repository. + - npm install + - npm run lint + - npm test + - npm run compile:js + - npm run compile:scss + - npm run compile:fonts + - node node_modules/.bin/dwupload --hostname ${HOSTNAME} --username ${USERNAME} --password "${PASSWORD}" --cartridge cartridges/app_storefront_base + - node node_modules/.bin/dwupload --hostname ${HOSTNAME} --username ${USERNAME} --password "${PASSWORD}" --cartridge cartridges/modules + - npm run test:integration -- --baseUrl https://${HOSTNAME}/on/demandware.store/Sites-MobileFirst-Site/en_US "test/integration/*" diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/.project b/storefront-reference-architecture/cartridges/app_storefront_base/.project new file mode 100644 index 0000000..01c634f --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/.project @@ -0,0 +1,17 @@ + + + app_storefront_base + + + + + + com.demandware.studio.core.beehiveElementBuilder + + + + + + com.demandware.studio.core.beehiveNature + + diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/.tern-project b/storefront-reference-architecture/cartridges/app_storefront_base/.tern-project new file mode 100644 index 0000000..cf68d98 --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/.tern-project @@ -0,0 +1,14 @@ +{ + "ecmaVersion": 5, + "plugins": { + "guess-types": { + + }, + "outline": { + + }, + "demandware": { + + } + } +} \ No newline at end of file diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/README.md b/storefront-reference-architecture/cartridges/app_storefront_base/README.md new file mode 100644 index 0000000..5307922 --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/README.md @@ -0,0 +1,5 @@ +# Welcome to Storefront Reference Architecture (SFRA) + +The Storefront Reference Architecture is fully compliant with standard JavaScript. It uses [Controllers]{@tutorial Controllers} to handle incoming requests. It provides a layer of JSON objects through the [Model-Views]{@tutorial Models}. All scripts are [Common JS modules](http://www.commonjs.org) with defined and documented exports to avoid polluting the global namespace. + +This documentation is meant to serve as a reference to quickly look up supported functionality and is fully based on the comments in the code. You can continue to maintain these [JSDoc comments](http://usejsdoc.org/) to generate a similar documentation for your own project. diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/app_storefront_base.properties b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/app_storefront_base.properties new file mode 100644 index 0000000..fb3d9ba --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/app_storefront_base.properties @@ -0,0 +1,4 @@ +## cartridge.properties for cartridge app_storefront_base +#Thu Jun 09 11:30:40 EDT 2016 +demandware.cartridges.app_storefront_base.multipleLanguageStorefront=true +demandware.cartridges.app_storefront_base.id=app_storefront_base diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/.eslintrc.json b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/.eslintrc.json new file mode 100644 index 0000000..b313049 --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "env": { + "jquery": true + }, + "rules": { + "global-require": "off", + "no-var": "off", + "prefer-const": "off" + } +} diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/addressBook.js b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/addressBook.js new file mode 100644 index 0000000..1ff5c24 --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/addressBook.js @@ -0,0 +1,7 @@ +'use strict'; + +var processInclude = require('./util'); + +$(document).ready(function () { + processInclude(require('./addressBook/addressBook')); +}); diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/addressBook/addressBook.js b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/addressBook/addressBook.js new file mode 100644 index 0000000..c17ff89 --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/addressBook/addressBook.js @@ -0,0 +1,105 @@ +'use strict'; + +var formValidation = require('../components/formValidation'); + +var url; +var isDefault; + +/** + * Create an alert to display the error message + * @param {Object} message - Error message to display + */ +function createErrorNotification(message) { + var errorHtml = ''; + + $('.error-messaging').append(errorHtml); +} + +module.exports = { + removeAddress: function () { + $('.remove-address').on('click', function (e) { + e.preventDefault(); + isDefault = $(this).data('default'); + if (isDefault) { + url = $(this).data('url') + + '?addressId=' + + $(this).data('id') + + '&isDefault=' + + isDefault; + } else { + url = $(this).data('url') + '?addressId=' + $(this).data('id'); + } + $('.product-to-remove').empty().append($(this).data('id')); + }); + }, + + removeAddressConfirmation: function () { + $('.delete-confirmation-btn').click(function (e) { + e.preventDefault(); + $.ajax({ + url: url, + type: 'get', + dataType: 'json', + success: function (data) { + $('#uuid-' + data.UUID).remove(); + if (isDefault) { + var addressId = $('.card .address-heading').first().text(); + var addressHeading = addressId + ' (' + data.defaultMsg + ')'; + $('.card .address-heading').first().text(addressHeading); + $('.card .card-make-default-link').first().remove(); + $('.remove-address').data('default', true); + if (data.message) { + var toInsert = '

' + + data.message + + '

'; + $('.addressList').after(toInsert); + } + } + }, + error: function (err) { + if (err.responseJSON.redirectUrl) { + window.location.href = err.responseJSON.redirectUrl; + } else { + createErrorNotification(err.responseJSON.errorMessage); + } + $.spinner().stop(); + } + }); + }); + }, + + submitAddress: function () { + $('form.address-form').submit(function (e) { + var $form = $(this); + e.preventDefault(); + url = $form.attr('action'); + $form.spinner().start(); + $('form.address-form').trigger('address:submit', e); + $.ajax({ + url: url, + type: 'post', + dataType: 'json', + data: $form.serialize(), + success: function (data) { + $form.spinner().stop(); + if (!data.success) { + formValidation($form, data); + } else { + location.href = data.redirectUrl; + } + }, + error: function (err) { + if (err.responseJSON.redirectUrl) { + window.location.href = err.responseJSON.redirectUrl; + } + $form.spinner().stop(); + } + }); + return false; + }); + } +}; diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/campaignBanner.js b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/campaignBanner.js new file mode 100644 index 0000000..733389b --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/campaignBanner.js @@ -0,0 +1,17 @@ +'use strict'; + +$(document).ready(function () { + if (window.resetCampaignBannerSessionToken) { + window.sessionStorage.removeItem('hide_campaign_banner'); + } + + var campaignBannerStatus = window.sessionStorage.getItem('hide_campaign_banner'); + $('.campaign-banner .close').on('click', function () { + $('.campaign-banner').addClass('d-none'); + window.sessionStorage.setItem('hide_campaign_banner', '1'); + }); + + if (!campaignBannerStatus || campaignBannerStatus < 0) { + $('.campaign-banner').removeClass('d-none'); + } +}); diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/carousel.js b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/carousel.js new file mode 100644 index 0000000..c20b566 --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/carousel.js @@ -0,0 +1,174 @@ +'use strict'; +var debounce = require('lodash/debounce'); + +/** + * Get display information related to screen size + * @param {jQuery} element - the current carousel that is being used + * @returns {Object} an object with display information + */ +function screenSize(element) { + var result = { + itemsToDisplay: null, + sufficientSlides: true + }; + var viewSize = $(window).width(); + var extraSmallDisplay = element.data('xs'); + var smallDisplay = element.data('sm'); + var mediumDisplay = element.data('md'); + var numberOfSlides = element.data('number-of-slides'); + + if (viewSize <= 575.98) { + result.itemsToDisplay = extraSmallDisplay; + } else if ((viewSize >= 576) && (viewSize <= 768.98)) { + result.itemsToDisplay = smallDisplay; + } else if (viewSize >= 769) { + result.itemsToDisplay = mediumDisplay; + } + + if (result.itemsToDisplay && numberOfSlides <= result.itemsToDisplay) { + result.sufficientSlides = false; + } + + return result; +} + +/** + * Makes the next element to be displayed next unreachable for screen readers and keyboard nav + * @param {jQuery} element - the current carousel that is being used + */ +function hiddenSlides(element) { + var carousel; + + if (element) { + carousel = element; + } else { + carousel = $('.experience-commerce_layouts-carousel .carousel, .experience-einstein-einsteinCarousel .carousel, .experience-einstein-einsteinCarouselCategory .carousel, .experience-einstein-einsteinCarouselProduct .carousel'); + } + + var screenSizeInfo = screenSize(carousel); + + var lastDisplayedElement; + var elementToBeDisplayed; + + switch (screenSizeInfo.itemsToDisplay) { + case 2: + lastDisplayedElement = carousel.find('.active.carousel-item + .carousel-item'); + elementToBeDisplayed = carousel.find('.active.carousel-item + .carousel-item + .carousel-item'); + break; + case 3: + lastDisplayedElement = carousel.find('.active.carousel-item + .carousel-item + .carousel-item'); + elementToBeDisplayed = carousel.find('.active.carousel-item + .carousel-item + .carousel-item + .carousel-item'); + break; + case 4: + lastDisplayedElement = carousel.find('.active.carousel-item + .carousel-item + .carousel-item + .carousel-item'); + elementToBeDisplayed = carousel.find('.active.carousel-item + .carousel-item + .carousel-item + .carousel-item + .carousel-item'); + break; + case 6: + lastDisplayedElement = carousel.find('.active.carousel-item + .carousel-item + .carousel-item + .carousel-item + .carousel-item + .carousel-item'); + elementToBeDisplayed = carousel.find('.active.carousel-item + .carousel-item + .carousel-item + .carousel-item + .carousel-item + .carousel-item + .carousel-item'); + break; + default: + break; + } + + carousel.find('.active.carousel-item').removeAttr('tabindex').removeAttr('aria-hidden'); + carousel.find('.active.carousel-item').find('a, button, details, input, textarea, select') + .removeAttr('tabindex') + .removeAttr('aria-hidden'); + + if (lastDisplayedElement) { + lastDisplayedElement.removeAttr('tabindex').removeAttr('aria-hidden'); + lastDisplayedElement.find('a, button, details, input, textarea, select') + .removeAttr('tabindex') + .removeAttr('aria-hidden'); + } + + if (elementToBeDisplayed) { + elementToBeDisplayed.attr('tabindex', -1).attr('aria-hidden', true); + elementToBeDisplayed.find('a, button, details, input, textarea, select') + .attr('tabindex', -1) + .attr('aria-hidden', true); + } +} + +$(document).ready(function () { + hiddenSlides(); + + $(window).on('resize', debounce(function () { + hiddenSlides(); + }, 500)); + + $('body').on('carousel:setup', function () { + hiddenSlides(); + }); + + $('.experience-commerce_layouts-carousel .carousel, .experience-einstein-einsteinCarousel .carousel, .experience-einstein-einsteinCarouselCategory .carousel, .experience-einstein-einsteinCarouselProduct .carousel').on('touchstart', function (touchStartEvent) { + var screenSizeInfo = screenSize($(this)); + + if (screenSizeInfo.sufficientSlides) { + var xClick = touchStartEvent.originalEvent.touches[0].pageX; + $(this).one('touchmove', function (touchMoveEvent) { + var xMove = touchMoveEvent.originalEvent.touches[0].pageX; + if (Math.floor(xClick - xMove) > 5) { + $(this).carousel('next'); + } else if (Math.floor(xClick - xMove) < -5) { + $(this).carousel('prev'); + } + }); + $('.experience-commerce_layouts-carousel .carousel, .experience-einstein-einsteinCarousel .carousel, .experience-einstein-einsteinCarouselCategory .carousel, .experience-einstein-einsteinCarouselProduct .carousel').on('touchend', function () { + $(this).off('touchmove'); + }); + } + }); + + $('.experience-commerce_layouts-carousel .carousel, .experience-einstein-einsteinCarousel .carousel, .experience-einstein-einsteinCarouselCategory .carousel, .experience-einstein-einsteinCarouselProduct .carousel').on('slide.bs.carousel', function (e) { + var activeCarouselPosition = $(e.relatedTarget).data('position'); + $(this).find('.pd-carousel-indicators .active').removeClass('active'); + $(this).find(".pd-carousel-indicators [data-position='" + activeCarouselPosition + "']").addClass('active'); + + var extraSmallDisplay = $(this).data('xs'); + var smallDisplay = $(this).data('sm'); + var mediumDisplay = $(this).data('md'); + + var arrayOfSlidesToDisplay = []; + + if (!$(this).hasClass('insufficient-xs-slides')) { + arrayOfSlidesToDisplay.push(extraSmallDisplay); + } + + if (!$(this).hasClass('insufficient-sm-slides')) { + arrayOfSlidesToDisplay.push(smallDisplay); + } + + if (!$(this).hasClass('insufficient-md-slides')) { + arrayOfSlidesToDisplay.push(mediumDisplay); + } + + var itemsToDisplay = Math.max.apply(Math, arrayOfSlidesToDisplay); + + var elementIndex = $(e.relatedTarget).index(); + var numberOfSlides = $('.carousel-item', this).length; + var carouselInner = $(this).find('.carousel-inner'); + var carouselItem; + + if (elementIndex >= numberOfSlides - (itemsToDisplay - 1)) { + var it = itemsToDisplay - (numberOfSlides - elementIndex); + for (var i = 0; i < it; i++) { + // append slides to end + if (e.direction === 'left') { + carouselItem = $('.carousel-item', this).eq(i); + + $(carouselItem).appendTo($(carouselInner)); + } else { + carouselItem = $('.carousel-item', this).eq(0); + + $(carouselItem).appendTo($(carouselInner)); + } + } + } + }); + + $('.experience-commerce_layouts-carousel .carousel, .experience-einstein-einsteinCarousel .carousel, .experience-einstein-einsteinCarouselCategory .carousel, .experience-einstein-einsteinCarouselProduct .carousel').on('slid.bs.carousel', function () { + hiddenSlides($(this)); + }); +}); diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/cart.js b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/cart.js new file mode 100644 index 0000000..c37bbab --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/cart.js @@ -0,0 +1,7 @@ +'use strict'; + +var processInclude = require('./util'); + +$(document).ready(function () { + processInclude(require('./cart/cart')); +}); diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/cart/cart.js b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/cart/cart.js new file mode 100644 index 0000000..7e1391f --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/cart/cart.js @@ -0,0 +1,766 @@ +'use strict'; + +var base = require('../product/base'); +var focusHelper = require('../components/focus'); + +/** + * appends params to a url + * @param {string} url - Original url + * @param {Object} params - Parameters to append + * @returns {string} result url with appended parameters + */ +function appendToUrl(url, params) { + var newUrl = url; + newUrl += (newUrl.indexOf('?') !== -1 ? '&' : '?') + Object.keys(params).map(function (key) { + return key + '=' + encodeURIComponent(params[key]); + }).join('&'); + + return newUrl; +} + +/** + * Checks whether the basket is valid. if invalid displays error message and disables + * checkout button + * @param {Object} data - AJAX response from the server + */ +function validateBasket(data) { + if (data.valid.error) { + if (data.valid.message) { + var errorHtml = ''; + + $('.cart-error').append(errorHtml); + } else { + $('.cart').empty().append('
' + + '
' + + '

' + data.resources.emptyCartMsg + '

' + + '
' + + '
' + ); + $('.number-of-items').empty().append(data.resources.numberOfItems); + $('.minicart-quantity').empty().append(data.numItems); + $('.minicart-link').attr({ + 'aria-label': data.resources.minicartCountOfItems, + title: data.resources.minicartCountOfItems + }); + $('.minicart .popover').empty(); + $('.minicart .popover').removeClass('show'); + } + + $('.checkout-btn').addClass('disabled'); + } else { + $('.checkout-btn').removeClass('disabled'); + } +} + +/** + * re-renders the order totals and the number of items in the cart + * @param {Object} data - AJAX response from the server + */ +function updateCartTotals(data) { + $('.number-of-items').empty().append(data.resources.numberOfItems); + $('.shipping-cost').empty().append(data.totals.totalShippingCost); + $('.tax-total').empty().append(data.totals.totalTax); + $('.grand-total').empty().append(data.totals.grandTotal); + $('.sub-total').empty().append(data.totals.subTotal); + $('.minicart-quantity').empty().append(data.numItems); + $('.minicart-link').attr({ + 'aria-label': data.resources.minicartCountOfItems, + title: data.resources.minicartCountOfItems + }); + if (data.totals.orderLevelDiscountTotal.value > 0) { + $('.order-discount').removeClass('hide-order-discount'); + $('.order-discount-total').empty() + .append('- ' + data.totals.orderLevelDiscountTotal.formatted); + } else { + $('.order-discount').addClass('hide-order-discount'); + } + + if (data.totals.shippingLevelDiscountTotal.value > 0) { + $('.shipping-discount').removeClass('hide-shipping-discount'); + $('.shipping-discount-total').empty().append('- ' + + data.totals.shippingLevelDiscountTotal.formatted); + } else { + $('.shipping-discount').addClass('hide-shipping-discount'); + } + + data.items.forEach(function (item) { + if (data.totals.orderLevelDiscountTotal.value > 0) { + $('.coupons-and-promos').empty().append(data.totals.discountsHtml); + } + if (item.renderedPromotions) { + $('.item-' + item.UUID).empty().append(item.renderedPromotions); + } else { + $('.item-' + item.UUID).empty(); + } + $('.uuid-' + item.UUID + ' .unit-price').empty().append(item.renderedPrice); + $('.line-item-price-' + item.UUID + ' .unit-price').empty().append(item.renderedPrice); + $('.item-total-' + item.UUID).empty().append(item.priceTotal.renderedPrice); + }); +} + +/** + * re-renders the order totals and the number of items in the cart + * @param {Object} message - Error message to display + */ +function createErrorNotification(message) { + var errorHtml = ''; + + $('.cart-error').append(errorHtml); +} + +/** + * re-renders the approaching discount messages + * @param {Object} approachingDiscounts - updated approaching discounts for the cart + */ +function updateApproachingDiscounts(approachingDiscounts) { + var html = ''; + $('.approaching-discounts').empty(); + if (approachingDiscounts.length > 0) { + approachingDiscounts.forEach(function (item) { + html += '
' + + item.discountMsg + '
'; + }); + } + $('.approaching-discounts').append(html); +} + +/** + * Updates the availability of a product line item + * @param {Object} data - AJAX response from the server + * @param {string} uuid - The uuid of the product line item to update + */ +function updateAvailability(data, uuid) { + var lineItem; + var messages = ''; + + for (var i = 0; i < data.items.length; i++) { + if (data.items[i].UUID === uuid) { + lineItem = data.items[i]; + break; + } + } + + if (lineItem != null) { + $('.availability-' + lineItem.UUID).empty(); + + if (lineItem.availability) { + if (lineItem.availability.messages) { + lineItem.availability.messages.forEach(function (message) { + messages += '

' + message + '

'; + }); + } + + if (lineItem.availability.inStockDate) { + messages += '

' + + lineItem.availability.inStockDate + + '

'; + } + } + + $('.availability-' + lineItem.UUID).html(messages); + } +} + +/** + * Finds an element in the array that matches search parameter + * @param {array} array - array of items to search + * @param {function} match - function that takes an element and returns a boolean indicating if the match is made + * @returns {Object|null} - returns an element of the array that matched the query. + */ +function findItem(array, match) { // eslint-disable-line no-unused-vars + for (var i = 0, l = array.length; i < l; i++) { + if (match.call(this, array[i])) { + return array[i]; + } + } + return null; +} + +/** + * Updates details of a product line item + * @param {Object} data - AJAX response from the server + * @param {string} uuid - The uuid of the product line item to update + */ +function updateProductDetails(data, uuid) { + $('.card.product-info.uuid-' + uuid).replaceWith(data.renderedTemplate); +} + +/** + * Generates the modal window on the first call. + * + */ +function getModalHtmlElement() { + if ($('#editProductModal').length !== 0) { + $('#editProductModal').remove(); + } + var htmlString = '' + + ''; + $('body').append(htmlString); +} + +/** + * Parses the html for a modal window + * @param {string} html - representing the body and footer of the modal window + * + * @return {Object} - Object with properties body and footer. + */ +function parseHtml(html) { + var $html = $('
').append($.parseHTML(html)); + + var body = $html.find('.product-quickview'); + var footer = $html.find('.modal-footer').children(); + + return { body: body, footer: footer }; +} + +/** + * replaces the content in the modal window for product variation to be edited. + * @param {string} editProductUrl - url to be used to retrieve a new product model + */ +function fillModalElement(editProductUrl) { + $('.modal-body').spinner().start(); + $.ajax({ + url: editProductUrl, + method: 'GET', + dataType: 'json', + success: function (data) { + var parsedHtml = parseHtml(data.renderedTemplate); + + $('#editProductModal .modal-body').empty(); + $('#editProductModal .modal-body').html(parsedHtml.body); + $('#editProductModal .modal-footer').html(parsedHtml.footer); + $('#editProductModal .modal-header .close .sr-only').text(data.closeButtonText); + $('#editProductModal .enter-message').text(data.enterDialogMessage); + $('#editProductModal').modal('show'); + $('body').trigger('editproductmodal:ready'); + $.spinner().stop(); + }, + error: function () { + $.spinner().stop(); + } + }); +} + +/** + * replace content of modal + * @param {string} actionUrl - url to be used to remove product + * @param {string} productID - pid + * @param {string} productName - product name + * @param {string} uuid - uuid + */ +function confirmDelete(actionUrl, productID, productName, uuid) { + var $deleteConfirmBtn = $('.cart-delete-confirmation-btn'); + var $productToRemoveSpan = $('.product-to-remove'); + + $deleteConfirmBtn.data('pid', productID); + $deleteConfirmBtn.data('action', actionUrl); + $deleteConfirmBtn.data('uuid', uuid); + + $productToRemoveSpan.empty().append(productName); +} + +module.exports = function () { + $('body').on('click', '.remove-product', function (e) { + e.preventDefault(); + + var actionUrl = $(this).data('action'); + var productID = $(this).data('pid'); + var productName = $(this).data('name'); + var uuid = $(this).data('uuid'); + confirmDelete(actionUrl, productID, productName, uuid); + }); + + $('body').on('afterRemoveFromCart', function (e, data) { + e.preventDefault(); + confirmDelete(data.actionUrl, data.productID, data.productName, data.uuid); + }); + + $('.optional-promo').click(function (e) { + e.preventDefault(); + $('.promo-code-form').toggle(); + }); + + $('body').on('click', '.cart-delete-confirmation-btn', function (e) { + e.preventDefault(); + + var productID = $(this).data('pid'); + var url = $(this).data('action'); + var uuid = $(this).data('uuid'); + var urlParams = { + pid: productID, + uuid: uuid + }; + + url = appendToUrl(url, urlParams); + + $('body > .modal-backdrop').remove(); + + $.spinner().start(); + + $('body').trigger('cart:beforeUpdate'); + + $.ajax({ + url: url, + type: 'get', + dataType: 'json', + success: function (data) { + if (data.basket.items.length === 0) { + $('.cart').empty().append('
' + + '
' + + '

' + data.basket.resources.emptyCartMsg + '

' + + '
' + + '
' + ); + $('.number-of-items').empty().append(data.basket.resources.numberOfItems); + $('.minicart-quantity').empty().append(data.basket.numItems); + $('.minicart-link').attr({ + 'aria-label': data.basket.resources.minicartCountOfItems, + title: data.basket.resources.minicartCountOfItems + }); + $('.minicart .popover').empty(); + $('.minicart .popover').removeClass('show'); + $('body').removeClass('modal-open'); + $('html').removeClass('veiled'); + } else { + if (data.toBeDeletedUUIDs && data.toBeDeletedUUIDs.length > 0) { + for (var i = 0; i < data.toBeDeletedUUIDs.length; i++) { + $('.uuid-' + data.toBeDeletedUUIDs[i]).remove(); + } + } + $('.uuid-' + uuid).remove(); + if (!data.basket.hasBonusProduct) { + $('.bonus-product').remove(); + } + $('.coupons-and-promos').empty().append(data.basket.totals.discountsHtml); + updateCartTotals(data.basket); + updateApproachingDiscounts(data.basket.approachingDiscounts); + $('body').trigger('setShippingMethodSelection', data.basket); + validateBasket(data.basket); + } + + $('body').trigger('cart:update', data); + + $.spinner().stop(); + }, + error: function (err) { + if (err.responseJSON.redirectUrl) { + window.location.href = err.responseJSON.redirectUrl; + } else { + createErrorNotification(err.responseJSON.errorMessage); + $.spinner().stop(); + } + } + }); + }); + + $('body').on('change', '.quantity-form > .quantity', function () { + var preSelectQty = $(this).data('pre-select-qty'); + var quantity = $(this).val(); + var productID = $(this).data('pid'); + var url = $(this).data('action'); + var uuid = $(this).data('uuid'); + + var urlParams = { + pid: productID, + quantity: quantity, + uuid: uuid + }; + url = appendToUrl(url, urlParams); + + $(this).parents('.card').spinner().start(); + + $('body').trigger('cart:beforeUpdate'); + + $.ajax({ + url: url, + type: 'get', + context: this, + dataType: 'json', + success: function (data) { + $('.quantity[data-uuid="' + uuid + '"]').val(quantity); + $('.coupons-and-promos').empty().append(data.totals.discountsHtml); + updateCartTotals(data); + updateApproachingDiscounts(data.approachingDiscounts); + updateAvailability(data, uuid); + validateBasket(data); + $(this).data('pre-select-qty', quantity); + + $('body').trigger('cart:update', data); + + $.spinner().stop(); + if ($(this).parents('.product-info').hasClass('bonus-product-line-item') && $('.cart-page').length) { + location.reload(); + } + }, + error: function (err) { + if (err.responseJSON.redirectUrl) { + window.location.href = err.responseJSON.redirectUrl; + } else { + createErrorNotification(err.responseJSON.errorMessage); + $(this).val(parseInt(preSelectQty, 10)); + $.spinner().stop(); + } + } + }); + }); + + $('.shippingMethods').change(function () { + var url = $(this).attr('data-actionUrl'); + var urlParams = { + methodID: $(this).find(':selected').attr('data-shipping-id') + }; + // url = appendToUrl(url, urlParams); + + $('.totals').spinner().start(); + $('body').trigger('cart:beforeShippingMethodSelected'); + $.ajax({ + url: url, + type: 'post', + dataType: 'json', + data: urlParams, + success: function (data) { + if (data.error) { + window.location.href = data.redirectUrl; + } else { + $('.coupons-and-promos').empty().append(data.totals.discountsHtml); + updateCartTotals(data); + updateApproachingDiscounts(data.approachingDiscounts); + validateBasket(data); + } + + $('body').trigger('cart:shippingMethodSelected', data); + $.spinner().stop(); + }, + error: function (err) { + if (err.redirectUrl) { + window.location.href = err.redirectUrl; + } else { + createErrorNotification(err.responseJSON.errorMessage); + $.spinner().stop(); + } + } + }); + }); + + $('.promo-code-form').submit(function (e) { + e.preventDefault(); + $.spinner().start(); + $('.coupon-missing-error').hide(); + $('.coupon-error-message').empty(); + if (!$('.coupon-code-field').val()) { + $('.promo-code-form .form-control').addClass('is-invalid'); + $('.promo-code-form .form-control').attr('aria-describedby', 'missingCouponCode'); + $('.coupon-missing-error').show(); + $.spinner().stop(); + return false; + } + var $form = $('.promo-code-form'); + $('.promo-code-form .form-control').removeClass('is-invalid'); + $('.coupon-error-message').empty(); + $('body').trigger('promotion:beforeUpdate'); + + $.ajax({ + url: $form.attr('action'), + type: 'GET', + dataType: 'json', + data: $form.serialize(), + success: function (data) { + if (data.error) { + $('.promo-code-form .form-control').addClass('is-invalid'); + $('.promo-code-form .form-control').attr('aria-describedby', 'invalidCouponCode'); + $('.coupon-error-message').empty().append(data.errorMessage); + $('body').trigger('promotion:error', data); + } else { + $('.coupons-and-promos').empty().append(data.totals.discountsHtml); + updateCartTotals(data); + updateApproachingDiscounts(data.approachingDiscounts); + validateBasket(data); + $('body').trigger('promotion:success', data); + } + $('.coupon-code-field').val(''); + $.spinner().stop(); + }, + error: function (err) { + $('body').trigger('promotion:error', err); + if (err.responseJSON.redirectUrl) { + window.location.href = err.responseJSON.redirectUrl; + } else { + createErrorNotification(err.errorMessage); + $.spinner().stop(); + } + } + }); + return false; + }); + + $('body').on('click', '.remove-coupon', function (e) { + e.preventDefault(); + + var couponCode = $(this).data('code'); + var uuid = $(this).data('uuid'); + var $deleteConfirmBtn = $('.delete-coupon-confirmation-btn'); + var $productToRemoveSpan = $('.coupon-to-remove'); + + $deleteConfirmBtn.data('uuid', uuid); + $deleteConfirmBtn.data('code', couponCode); + + $productToRemoveSpan.empty().append(couponCode); + }); + + $('body').on('click', '.delete-coupon-confirmation-btn', function (e) { + e.preventDefault(); + + var url = $(this).data('action'); + var uuid = $(this).data('uuid'); + var couponCode = $(this).data('code'); + var urlParams = { + code: couponCode, + uuid: uuid + }; + + url = appendToUrl(url, urlParams); + + $('body > .modal-backdrop').remove(); + + $.spinner().start(); + $('body').trigger('promotion:beforeUpdate'); + $.ajax({ + url: url, + type: 'get', + dataType: 'json', + success: function (data) { + $('.coupon-uuid-' + uuid).remove(); + updateCartTotals(data); + updateApproachingDiscounts(data.approachingDiscounts); + validateBasket(data); + $.spinner().stop(); + $('body').trigger('promotion:success', data); + }, + error: function (err) { + $('body').trigger('promotion:error', err); + if (err.responseJSON.redirectUrl) { + window.location.href = err.responseJSON.redirectUrl; + } else { + createErrorNotification(err.responseJSON.errorMessage); + $.spinner().stop(); + } + } + }); + }); + $('body').on('click', '.cart-page .bonus-product-button', function () { + $.spinner().start(); + $(this).addClass('launched-modal'); + $.ajax({ + url: $(this).data('url'), + method: 'GET', + dataType: 'json', + success: function (data) { + base.methods.editBonusProducts(data); + $.spinner().stop(); + }, + error: function () { + $.spinner().stop(); + } + }); + }); + + $('body').on('hidden.bs.modal', '#chooseBonusProductModal', function () { + $('#chooseBonusProductModal').remove(); + $('.modal-backdrop').remove(); + $('body').removeClass('modal-open'); + + if ($('.cart-page').length) { + $('.launched-modal .btn-outline-primary').trigger('focus'); + $('.launched-modal').removeClass('launched-modal'); + } else { + $('.product-detail .add-to-cart').focus(); + } + }); + + $('body').on('click', '.cart-page .product-edit .edit, .cart-page .bundle-edit .edit', function (e) { + e.preventDefault(); + + var editProductUrl = $(this).attr('href'); + getModalHtmlElement(); + fillModalElement(editProductUrl); + }); + + $('body').on('shown.bs.modal', '#editProductModal', function () { + $('#editProductModal').siblings().attr('aria-hidden', 'true'); + $('#editProductModal .close').focus(); + }); + + $('body').on('hidden.bs.modal', '#editProductModal', function () { + $('#editProductModal').siblings().attr('aria-hidden', 'false'); + }); + + $('body').on('keydown', '#editProductModal', function (e) { + var focusParams = { + event: e, + containerSelector: '#editProductModal', + firstElementSelector: '.close', + lastElementSelector: '.update-cart-product-global', + nextToLastElementSelector: '.modal-footer .quantity-select' + }; + focusHelper.setTabNextFocus(focusParams); + }); + + $('body').on('product:updateAddToCart', function (e, response) { + // update global add to cart (single products, bundles) + var dialog = $(response.$productContainer) + .closest('.quick-view-dialog'); + + $('.update-cart-product-global', dialog).attr('disabled', + !$('.global-availability', dialog).data('ready-to-order') + || !$('.global-availability', dialog).data('available') + ); + }); + + $('body').on('product:updateAvailability', function (e, response) { + // bundle individual products + $('.product-availability', response.$productContainer) + .data('ready-to-order', response.product.readyToOrder) + .data('available', response.product.available) + .find('.availability-msg') + .empty() + .html(response.message); + + + var dialog = $(response.$productContainer) + .closest('.quick-view-dialog'); + + if ($('.product-availability', dialog).length) { + // bundle all products + var allAvailable = $('.product-availability', dialog).toArray() + .every(function (item) { return $(item).data('available'); }); + + var allReady = $('.product-availability', dialog).toArray() + .every(function (item) { return $(item).data('ready-to-order'); }); + + $('.global-availability', dialog) + .data('ready-to-order', allReady) + .data('available', allAvailable); + + $('.global-availability .availability-msg', dialog).empty() + .html(allReady ? response.message : response.resources.info_selectforstock); + } else { + // single product + $('.global-availability', dialog) + .data('ready-to-order', response.product.readyToOrder) + .data('available', response.product.available) + .find('.availability-msg') + .empty() + .html(response.message); + } + }); + + $('body').on('product:afterAttributeSelect', function (e, response) { + if ($('.modal.show .product-quickview .bundle-items').length) { + $('.modal.show').find(response.container).data('pid', response.data.product.id); + $('.modal.show').find(response.container).find('.product-id').text(response.data.product.id); + } else { + $('.modal.show .product-quickview').data('pid', response.data.product.id); + } + }); + + $('body').on('change', '.quantity-select', function () { + var selectedQuantity = $(this).val(); + $('.modal.show .update-cart-url').data('selected-quantity', selectedQuantity); + }); + + $('body').on('change', '.options-select', function () { + var selectedOptionValueId = $(this).children('option:selected').data('value-id'); + $('.modal.show .update-cart-url').data('selected-option', selectedOptionValueId); + }); + + $('body').on('click', '.update-cart-product-global', function (e) { + e.preventDefault(); + + var updateProductUrl = $(this).closest('.cart-and-ipay').find('.update-cart-url').val(); + var selectedQuantity = $(this).closest('.cart-and-ipay').find('.update-cart-url').data('selected-quantity'); + var selectedOptionValueId = $(this).closest('.cart-and-ipay').find('.update-cart-url').data('selected-option'); + var uuid = $(this).closest('.cart-and-ipay').find('.update-cart-url').data('uuid'); + + var form = { + uuid: uuid, + pid: base.getPidValue($(this)), + quantity: selectedQuantity, + selectedOptionValueId: selectedOptionValueId + }; + + $(this).parents('.card').spinner().start(); + + $('body').trigger('cart:beforeUpdate'); + + if (updateProductUrl) { + $.ajax({ + url: updateProductUrl, + type: 'post', + context: this, + data: form, + dataType: 'json', + success: function (data) { + $('#editProductModal').modal('hide'); + + $('.coupons-and-promos').empty().append(data.cartModel.totals.discountsHtml); + updateCartTotals(data.cartModel); + updateApproachingDiscounts(data.cartModel.approachingDiscounts); + updateAvailability(data.cartModel, uuid); + updateProductDetails(data, uuid); + + if (data.uuidToBeDeleted) { + $('.uuid-' + data.uuidToBeDeleted).remove(); + } + + validateBasket(data.cartModel); + + $('body').trigger('cart:update', data); + + $.spinner().stop(); + }, + error: function (err) { + if (err.responseJSON.redirectUrl) { + window.location.href = err.responseJSON.redirectUrl; + } else { + createErrorNotification(err.responseJSON.errorMessage); + $.spinner().stop(); + } + } + }); + } + }); + + base.selectAttribute(); + base.colorAttribute(); + base.removeBonusProduct(); + base.selectBonusProduct(); + base.enableBonusProductSelection(); + base.showMoreBonusProducts(); + base.addBonusProductsToCart(); + base.focusChooseBonusProductModal(); + base.trapChooseBonusProductModalFocus(); + base.onClosingChooseBonusProductModal(); +}; diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/checkout.js b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/checkout.js new file mode 100644 index 0000000..9e0f5e5 --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/checkout.js @@ -0,0 +1,7 @@ +'use strict'; + +var processInclude = require('./util'); + +$(document).ready(function () { + processInclude(require('./checkout/checkout')); +}); diff --git a/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/checkout/address.js b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/checkout/address.js new file mode 100644 index 0000000..182948a --- /dev/null +++ b/storefront-reference-architecture/cartridges/app_storefront_base/cartridge/client/default/js/checkout/address.js @@ -0,0 +1,187 @@ +'use strict'; + +/** + * Populate the Billing Address Summary View + * @param {string} parentSelector - the top level DOM selector for a unique address summary + * @param {Object} address - the address data + */ +function populateAddressSummary(parentSelector, address) { + $.each(address, function (attr) { + var val = address[attr]; + $('.' + attr, parentSelector).text(val || ''); + }); +} + +/** + * returns a formed '); + } + var safeShipping = shipping || {}; + var shippingAddress = safeShipping.shippingAddress || {}; + + if (isBilling && isNew && !order.billing.matchingAddressId) { + shippingAddress = order.billing.billingAddress.address || {}; + isNew = false; + isSelected = true; + safeShipping.UUID = 'manual-entry'; + } + + var uuid = safeShipping.UUID ? safeShipping.UUID : 'new'; + var optionEl = $('