├── cypress.json ├── customRoutes ├── redirects.json └── categories.json ├── helpers ├── cache │ ├── README.md │ └── browser.js ├── README.md └── buildQueries.ts ├── i18nRoutes.js ├── .dockerignore ├── custom-api ├── api │ ├── index.js │ └── loadCart.js ├── types │ └── index.ts ├── fragments │ ├── productFragment.ts │ └── orderFragment.ts └── customQueries.js ├── static ├── icon.png ├── favicon.ico ├── homepage │ ├── apple.png │ ├── bannerD.png │ ├── google.png │ ├── bannerA.webp │ ├── bannerB.webp │ ├── bannerC.webp │ ├── bannerE.webp │ ├── bannerF.webp │ ├── bannerG.webp │ ├── bannerH.webp │ ├── imageAd.webp │ ├── imageAm.webp │ ├── imageBd.webp │ ├── imageBm.webp │ ├── imageCd.webp │ ├── imageCm.webp │ ├── imageDd.webp │ ├── imageDm.webp │ ├── productA.webp │ ├── productB.webp │ ├── productC.webp │ └── newsletter.webp ├── icons │ ├── langs │ │ ├── ru.png │ │ ├── de.webp │ │ └── en.webp │ ├── facebook.svg │ ├── youtube.svg │ ├── twitter.svg │ ├── pinterest.svg │ └── logo.svg ├── thankyou │ ├── bannerD.png │ └── bannerM.png ├── productpage │ ├── productA.jpg │ ├── productB.jpg │ └── productM.jpg └── README.md ├── docs └── odoo_readme.png ├── jest.config.js ├── middleware ├── auth.js ├── is-authenticated.js └── checkout.js ├── .lintstagedrc ├── serverMiddleware └── body-parser.js ├── app └── router.scrollBehavior.js ├── composables ├── index.ts ├── useUiNotification │ └── index.ts ├── useProduct │ └── index.ts ├── useUiState.ts ├── useCart │ └── index.ts └── useUiHelpers │ └── index.ts ├── hooks ├── execute.js ├── index.js ├── buildRedirects.js └── buildRoutes.js ├── .editorconfig ├── lighthouserc.json ├── Dockerfile ├── components ├── Checkout │ ├── WireTransferPaymentProvider.vue │ ├── AbstractPaymentObserver.vue │ ├── AdyenPaymentProvider.vue │ ├── AdyenExternalPaymentProvider.vue │ ├── VsfPaymentProvider.vue │ ├── VsfShippingProvider.vue │ ├── UserBillingAddresses.vue │ ├── UserShippingAddresses.vue │ ├── AdyenDirectPaymentProvider.vue │ └── CartPreview.vue ├── TopBar.vue ├── UserBillingAddress.vue ├── UserShippingAddress.vue ├── MobileStoreBanner.vue ├── Notification.vue ├── MyAccount │ ├── ShippingList.vue │ ├── NewsletterForm.vue │ ├── PasswordResetForm.vue │ ├── ProfileUpdateForm.vue │ ├── OrderHistory.vue │ └── ShippingAddressForm.vue ├── LocaleSelector.vue ├── NewsletterModal.vue ├── BottomNavigation.vue ├── RelatedProducts.vue ├── InstagramFeed.vue ├── Core │ └── Atoms │ │ └── OdooButton.vue └── MobileMenuSidebar.vue ├── .env.example ├── tests └── e2e │ ├── cypress.json │ ├── api │ └── requests.ts │ ├── fixtures │ └── categoryMocksData.json │ ├── support │ ├── index.js │ └── commands.js │ ├── plugins │ └── index.js │ └── integration │ └── addToCart.spec.ts ├── assets ├── email.svg ├── css │ ├── mobileMenuSideBar.scss │ ├── account │ │ ├── orderHistory.scss │ │ └── myProfile.scss │ ├── category.scss │ └── product.scss ├── login │ ├── time.svg │ └── Favourites.svg └── styles.scss ├── tsconfig.json ├── plugins ├── getImage.ts └── vee-validate.ts ├── LICENSE ├── middleware.config.js ├── themeConfig.js ├── pages ├── Checkout │ └── FailedPaymentResponse.vue ├── Checkout.vue └── MyAccount.vue ├── sitemap └── index.ts ├── layouts ├── blank.vue ├── error.vue └── default.vue ├── .gitignore ├── routes.js ├── package.json ├── docker-compose.yml ├── README.md └── tailwind.config.js /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /customRoutes/redirects.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /helpers/cache/README.md: -------------------------------------------------------------------------------- 1 | # VSF Next Cache -------------------------------------------------------------------------------- /i18nRoutes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | node_modules/ -------------------------------------------------------------------------------- /custom-api/api/index.js: -------------------------------------------------------------------------------- 1 | import loadCart from './loadCart'; 2 | 3 | export { 4 | loadCart 5 | }; 6 | -------------------------------------------------------------------------------- /helpers/README.md: -------------------------------------------------------------------------------- 1 | Put here platform-specific, non-agnostic functions that overwrite default code. 2 | -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/icon.png -------------------------------------------------------------------------------- /docs/odoo_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/docs/odoo_readme.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('./../../jest.base.config'); 2 | 3 | module.exports = baseConfig; 4 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/homepage/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/apple.png -------------------------------------------------------------------------------- /static/icons/langs/ru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/icons/langs/ru.png -------------------------------------------------------------------------------- /static/homepage/bannerD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/bannerD.png -------------------------------------------------------------------------------- /static/homepage/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/google.png -------------------------------------------------------------------------------- /static/icons/langs/de.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/icons/langs/de.webp -------------------------------------------------------------------------------- /static/icons/langs/en.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/icons/langs/en.webp -------------------------------------------------------------------------------- /static/thankyou/bannerD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/thankyou/bannerD.png -------------------------------------------------------------------------------- /static/thankyou/bannerM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/thankyou/bannerM.png -------------------------------------------------------------------------------- /static/homepage/bannerA.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/bannerA.webp -------------------------------------------------------------------------------- /static/homepage/bannerB.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/bannerB.webp -------------------------------------------------------------------------------- /static/homepage/bannerC.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/bannerC.webp -------------------------------------------------------------------------------- /static/homepage/bannerE.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/bannerE.webp -------------------------------------------------------------------------------- /static/homepage/bannerF.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/bannerF.webp -------------------------------------------------------------------------------- /static/homepage/bannerG.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/bannerG.webp -------------------------------------------------------------------------------- /static/homepage/bannerH.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/bannerH.webp -------------------------------------------------------------------------------- /static/homepage/imageAd.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/imageAd.webp -------------------------------------------------------------------------------- /static/homepage/imageAm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/imageAm.webp -------------------------------------------------------------------------------- /static/homepage/imageBd.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/imageBd.webp -------------------------------------------------------------------------------- /static/homepage/imageBm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/imageBm.webp -------------------------------------------------------------------------------- /static/homepage/imageCd.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/imageCd.webp -------------------------------------------------------------------------------- /static/homepage/imageCm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/imageCm.webp -------------------------------------------------------------------------------- /static/homepage/imageDd.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/imageDd.webp -------------------------------------------------------------------------------- /static/homepage/imageDm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/imageDm.webp -------------------------------------------------------------------------------- /static/homepage/productA.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/productA.webp -------------------------------------------------------------------------------- /static/homepage/productB.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/productB.webp -------------------------------------------------------------------------------- /static/homepage/productC.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/productC.webp -------------------------------------------------------------------------------- /static/homepage/newsletter.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/homepage/newsletter.webp -------------------------------------------------------------------------------- /static/productpage/productA.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/productpage/productA.jpg -------------------------------------------------------------------------------- /static/productpage/productB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/productpage/productB.jpg -------------------------------------------------------------------------------- /static/productpage/productM.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront-community/template-odoo/HEAD/static/productpage/productM.jpg -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | export default function ({ app, redirect }) { 2 | if (!app.$cookies.get("odoo-user")) { 3 | return redirect("/"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /middleware/is-authenticated.js: -------------------------------------------------------------------------------- 1 | export default async ({ app, redirect }) => { 2 | if (!app.$cookies.get('odoo-user')) { 3 | return redirect('/'); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,jsx}": [ 3 | "eslint --fix" 4 | ], 5 | "*.{ts,tsx}": [ 6 | "eslint --fix" 7 | ], 8 | "*.vue": [ 9 | "eslint --fix" 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /serverMiddleware/body-parser.js: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser'; 2 | import express from 'express'; 3 | 4 | const app = express(); 5 | 6 | app.use(bodyParser.json()); 7 | export default app; 8 | -------------------------------------------------------------------------------- /app/router.scrollBehavior.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | export default function(_to, _from, savedPosition) { 3 | return ( 4 | savedPosition || { 5 | x: 0, 6 | y: 0 7 | } 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /composables/index.ts: -------------------------------------------------------------------------------- 1 | import useUiHelpers from './useUiHelpers'; 2 | import useUiNotification from './useUiNotification'; 3 | import useUiState from './useUiState'; 4 | 5 | export { 6 | useUiHelpers, 7 | useUiNotification, 8 | useUiState 9 | }; 10 | -------------------------------------------------------------------------------- /hooks/execute.js: -------------------------------------------------------------------------------- 1 | const buildRedirects = require('./buildRedirects'); 2 | const buildRoutes = require('./buildRoutes'); 3 | 4 | switch (process.argv[2]) { 5 | case 'routes': buildRoutes(); break; 6 | case 'redirects': buildRedirects(); break; 7 | default:break; 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /hooks/index.js: -------------------------------------------------------------------------------- 1 | import buildRoutes from './buildRoutes'; 2 | import buildRedirects from './buildRedirects'; 3 | export default { 4 | build: { 5 | async before(builder) { 6 | await buildRoutes(); 7 | await buildRedirects(builder); 8 | } 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /lighthouserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "ci": { 3 | "collect": { 4 | "startServerCommand": "yarn build && yarn start", 5 | "url": ["http://localhost:3000/"], 6 | "numberOfRuns": 3 7 | }, 8 | "upload": { 9 | "target": "temporary-public-storage" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /static/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine3.17 2 | 3 | WORKDIR /app 4 | 5 | COPY . /app 6 | 7 | RUN yarn install \ 8 | --prefer-offline \ 9 | --frozen-lockfile \ 10 | --non-interactive \ 11 | --production=false 12 | 13 | COPY . . 14 | 15 | ENV NODE_ENV=production 16 | 17 | RUN yarn build 18 | 19 | EXPOSE 3000 20 | 21 | ENV NUXT_HOST=0.0.0.0 22 | ENV NUXT_PORT=3000 23 | 24 | 25 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /helpers/buildQueries.ts: -------------------------------------------------------------------------------- 1 | const products = ` 2 | query { 3 | products (pageSize: 150000) { 4 | products { 5 | id 6 | slug 7 | } 8 | } 9 | } 10 | `; 11 | 12 | const categories = ` 13 | query { 14 | categories (pageSize: 10000) { 15 | categories { 16 | id 17 | name 18 | slug 19 | parent { 20 | id 21 | slug 22 | } 23 | } 24 | } 25 | } 26 | `; 27 | 28 | export { products, categories }; 29 | -------------------------------------------------------------------------------- /components/Checkout/WireTransferPaymentProvider.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /custom-api/api/loadCart.js: -------------------------------------------------------------------------------- 1 | const gql = require('graphql-tag'); 2 | const orderFragment = require('../fragments/orderFragment.ts'); 3 | 4 | const loadCart = async (context, variables) => { 5 | return await context.client.apollo.query({ 6 | fetchPolicy: 'no-cache', 7 | variables, 8 | query: gql` 9 | query { 10 | cart { 11 | ${orderFragment} 12 | } 13 | } 14 | `, 15 | }); 16 | }; 17 | 18 | module.exports = loadCart; 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | BASE_URL=https://vsfdemo15.labs.odoogap.com/ 2 | SITE_URL=http://localhost:3000/ 3 | # PUBLIC_PATH=https://xyz.cloudfront.cdn.com 4 | # For CI and other deployment scripts 5 | 6 | NODE_ENV=dev 7 | NODE_LOCALE=en-EN 8 | PORT=3000 9 | HOST=0.0.0.0 10 | 11 | REDIS_ENABLED=false 12 | REDIS_HOST=127.0.0.1 13 | REDIS_PORT=6379 14 | REDIS_PASSWORD=pass 15 | # Used for invalidating cache 16 | INVALIDATION_KEY=123 17 | 18 | NUXT_TELEMETRY_DISABLED=1 19 | GOOGLE_TAG_MANAGER_ID=1 -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your static files. 6 | Each file inside this directory is mapped to `/`. 7 | Thus you'd want to delete this README.md before deploying to production. 8 | 9 | Example: `/static/robots.txt` is mapped as `/robots.txt`. 10 | 11 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static). 12 | -------------------------------------------------------------------------------- /tests/e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "fixturesFolder": "tests/e2e/fixtures", 4 | "integrationFolder": "tests/e2e/integration", 5 | "pluginsFile": "tests/e2e/plugins/index.js", 6 | "supportFile": "tests/e2e/support/index.js", 7 | "viewportHeight": 1080, 8 | "viewportWidth": 1920, 9 | "requestTimeout": 30000, 10 | "numTestsKeptInMemory": 0, 11 | "responseTimeout": 50000, 12 | "pageLoadTimeout": 100000, 13 | "video": false 14 | } -------------------------------------------------------------------------------- /tests/e2e/api/requests.ts: -------------------------------------------------------------------------------- 1 | 2 | import categoryMocks from '../fixtures/categoryMocksData.json'; 3 | 4 | const requests = { 5 | getCategories (): Cypress.Chainable { 6 | const options = { 7 | url: '/api/odoo/getCategory', 8 | method: 'POST', 9 | headers: { 10 | Accept: 'application/json', 11 | 'Content-Type': 'application/json' 12 | }, 13 | body: categoryMocks 14 | }; 15 | return cy.request(options); 16 | } 17 | }; 18 | 19 | export default requests; 20 | -------------------------------------------------------------------------------- /components/Checkout/AbstractPaymentObserver.vue: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /custom-api/types/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | 3 | export enum ButtonType { 4 | Primary = 'Primary', 5 | Secondary = 'Secondary', 6 | Tertiary = 'Tertiary', 7 | } 8 | 9 | export enum ButtonShape { 10 | Round = 'Round', 11 | Rectangle = 'Rectangle', 12 | } 13 | 14 | export enum ButtonSize { 15 | Small = 'Small', 16 | Medium = 'Medium', 17 | Large = 'Large', 18 | Max = 'Max', 19 | } 20 | export enum ButtonColor { 21 | Green = 'Green', 22 | Black = 'Black', 23 | Grey = 'Grey', 24 | } 25 | 26 | -------------------------------------------------------------------------------- /custom-api/fragments/productFragment.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | id 3 | firstVariant{ 4 | id 5 | combinationInfoVariant 6 | } 7 | smallImage 8 | price 9 | name 10 | description 11 | image 12 | imageFilename 13 | slug 14 | sku 15 | jsonLd 16 | isInWishlist 17 | categories { 18 | id 19 | name 20 | slug 21 | parent{ 22 | parent{ 23 | id 24 | } 25 | } 26 | } 27 | attributeValues { 28 | id 29 | name 30 | displayType 31 | priceExtra 32 | attribute { 33 | id 34 | name 35 | } 36 | search 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /static/icons/youtube.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/email.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/css/mobileMenuSideBar.scss: -------------------------------------------------------------------------------- 1 | .mobileMenu { 2 | --sidebar-z-index: 3; 3 | --overlay-z-index: 3; 4 | --sidebar-top-padding: var(--spacer-lg) var(--spacer-base) 0 5 | var(--spacer-base); 6 | --sidebar-content-padding: var(--spacer-lg) var(--spacer-base); 7 | } 8 | 9 | .heading { 10 | &__wrapper { 11 | --heading-title-color: var(--c-link); 12 | --heading-title-font-weight: var(--font-weight--semibold); 13 | display: flex; 14 | justify-content: space-between; 15 | } 16 | } 17 | 18 | .sidebar-bottom { 19 | margin: auto 0 0 0; 20 | } 21 | 22 | ::v-deep .sf-list .sf-list__item { 23 | padding: 10px 0; 24 | } 25 | -------------------------------------------------------------------------------- /components/Checkout/AdyenPaymentProvider.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 | 37 | -------------------------------------------------------------------------------- /assets/login/time.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/e2e/fixtures/categoryMocksData.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "11", 4 | "name": "Women", 5 | "slug": "women", 6 | "parent": null, 7 | "topCategory": true 8 | }, 9 | { 10 | "id": "12", 11 | "name": "Men", 12 | "slug": "men", 13 | "parent": null, 14 | "topCategory": true 15 | }, 16 | { 17 | "id": "13", 18 | "name": "Clothing", 19 | "slug": "women-clothing", 20 | "parent": [ 21 | { 22 | "id": "11", 23 | "name": "Women" 24 | } 25 | ] 26 | }, 27 | { 28 | "id": "17", 29 | "name": "All", 30 | "slug": "women-clothing-all", 31 | "parent": [ 32 | { 33 | "id": "13", 34 | "name": "Clothing" 35 | } 36 | ] 37 | } 38 | ] -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "resolveJsonModule": true, 4 | "target": "es2018", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "lib": [ 8 | "esnext", 9 | "esnext.asynciterable", 10 | "dom" 11 | ], 12 | "esModuleInterop": true, 13 | "allowJs": true, 14 | "sourceMap": true, 15 | "strict": false, 16 | "noEmit": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "~/*": [ 20 | "./*" 21 | ], 22 | "@/*": [ 23 | "./*" 24 | ] 25 | }, 26 | "types": [ 27 | "@types/node", 28 | "@nuxt/types", 29 | "nuxt-i18n" 30 | ] 31 | }, 32 | "exclude": [ 33 | "node_modules", 34 | "tests" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /static/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | } 23 | -------------------------------------------------------------------------------- /plugins/getImage.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '@nuxt/types'; 2 | declare module 'vue/types/vue' { 3 | interface Vue { 4 | $image(imagePath: string, width: number, heigth: number, name: string): string 5 | } 6 | } 7 | declare module '@nuxt/types' { 8 | interface NuxtAppOptions { 9 | $image(imagePath: string, width: number, heigth: number, name: string): string 10 | } 11 | interface Context { 12 | $image(imagePath: string, width: number, heigth: number, name: string): string 13 | } 14 | } 15 | const getImagePlugin: Plugin = (context, inject) => { 16 | inject('image', (imagePath: string, width: number, heigth: number, name: string) => { 17 | const resolution = `${width}x${heigth}`; 18 | return `${context.app.$config.baseURL}${imagePath?.replace('/', '')}/${resolution}/${name}_${resolution}`; 19 | }); 20 | }; 21 | 22 | export default getImagePlugin; 23 | -------------------------------------------------------------------------------- /components/TopBar.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | 37 | -------------------------------------------------------------------------------- /tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /hooks/buildRedirects.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const consola = require('consola'); 3 | const chalk = require('chalk'); 4 | const axios = require('axios'); 5 | const fsExtra = require('fs-extra'); 6 | const odooBaseUrl = process.env.BACKEND_BASE_URL || process.env.BASE_URL; 7 | 8 | const redirectUrl = `${odooBaseUrl}vsf/redirects`; 9 | 10 | module.exports = () => { 11 | if (!odooBaseUrl) { 12 | consola.error(chalk.bold('ODOO'), ' - You need create a .env or set BACKEND_BASE_URL || BASE_URL '); 13 | return; 14 | } 15 | 16 | consola.info(chalk.bold('ODOO'), ' - Started fetch ODOO redirects...'); 17 | 18 | axios.get(redirectUrl) 19 | .then(({ data }) => { 20 | fsExtra.writeJson('customRoutes/redirects.json', data).then(() => { 21 | consola.success(chalk.bold('ODOO'), ' - Redirects.json written!'); 22 | }); 23 | }).catch((error) => { 24 | consola.error(chalk.bold('ODOO'), ' - Redirects request failed'); 25 | consola.error(error); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /static/icons/pinterest.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vue Storefront 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 | -------------------------------------------------------------------------------- /plugins/vee-validate.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { extend } from 'vee-validate'; 3 | import { min, email, required, digits, max, confirmed } from 'vee-validate/dist/rules'; 4 | import { Plugin } from '@nuxt/types'; 5 | 6 | const veeValidatePlugin : Plugin = ({ i18n }) => { 7 | extend('min', { 8 | ...min, 9 | message: field => `${i18n.t('This field must have be')} ${field} ${i18n.t('digits')}` 10 | }); 11 | 12 | extend('confirmed', { 13 | ...confirmed, 14 | message: field => i18n.t('This field must have be checked').toString() 15 | }); 16 | 17 | extend('max', { 18 | ...max, 19 | message: field => `${i18n.t('This field must have be')} ${field}` 20 | }); 21 | 22 | extend('email', { 23 | ...email, 24 | message: i18n.t('This field must be an email')?.toString() 25 | }); 26 | 27 | extend('digits', { 28 | ...digits, 29 | message: field => `${i18n.t('This field must have')} ${field} ${i18n.t('digits')}` 30 | }); 31 | 32 | extend('required', { 33 | ...required, 34 | message: i18n.t('This field is required.')?.toString() 35 | }); 36 | 37 | }; 38 | 39 | export default veeValidatePlugin; 40 | -------------------------------------------------------------------------------- /helpers/cache/browser.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | /* eslint-disable no-undef */ 3 | 4 | const cacheOptions = { 5 | ignoreSearch: true, 6 | ignoreMethod: true, 7 | ignoreVary: true 8 | }; 9 | 10 | const timeoutPlugin = new workbox.expiration.ExpirationPlugin({ 11 | // Timeout 1 day // Only cache 150 resources. 12 | maxEntries: 150, 13 | purgeOnQuotaError: true, 14 | maxAgeSeconds: 24 * 60 * 60 15 | }); 16 | 17 | const cachePlugin = { 18 | cacheKeyWillBeUsed: async ({ request }) => { 19 | 20 | const cacheKey = new Request(request.referrer, { 21 | headers: request.headers, 22 | method: 'GET' 23 | }); 24 | 25 | return cacheKey; 26 | } 27 | }; 28 | 29 | const apiHandler = new workbox.strategies.StaleWhileRevalidate({ 30 | cacheName: 'odoo-apis', 31 | cacheOptions, 32 | plugins: [cachePlugin, timeoutPlugin] 33 | }); 34 | 35 | const imageHandler = new workbox.strategies.StaleWhileRevalidate({ 36 | cacheName: 'odoo-images', 37 | cacheOptions: { ignoreVary: true }, 38 | plugins: [timeoutPlugin] 39 | }); 40 | 41 | workbox.routing.registerRoute(({ event }) => event.request.headers.get('Accept').includes('image'), imageHandler); 42 | -------------------------------------------------------------------------------- /middleware.config.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const odooRequestIsHttps = process.env.BASE_URL?.toLowerCase()?.includes('https') 3 | 4 | const odooBaseUrl = process.env.BACKEND_BASE_URL || process.env.BASE_URL; 5 | const graphqlBaseUrl = `${odooBaseUrl}graphql/vsf`; 6 | const baseDomain = process.env.SITE_URL?.replace('https://', '')?.slice(0, -1) || ''; 7 | 8 | const extendApiMethods = require('./custom-api/api'); 9 | const customQueries = require('./custom-api/customQueries'); 10 | 11 | const fetchOptions = odooRequestIsHttps ? { 12 | agent: new https.Agent({ 13 | rejectUnauthorized: false 14 | }) 15 | } : {}; 16 | 17 | module.exports = { 18 | integrations: { 19 | odoo: { 20 | location: '@vue-storefront/odoo-api/server', 21 | configuration: { 22 | odooBaseUrl, 23 | graphqlBaseUrl, 24 | baseDomain, 25 | redisClient: (process.env.REDIS_ENABLED === 'true'), 26 | fetchOptions 27 | }, 28 | extensions: (extensions) => [ 29 | ...extensions, 30 | { 31 | name: 'odoo-extension', 32 | extendApiMethods 33 | } 34 | ], 35 | customQueries 36 | } 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /middleware/checkout.js: -------------------------------------------------------------------------------- 1 | const canEnterShipping = (cart) => cart?.order.orderLines?.length > 0 || false; 2 | 3 | const canEnterBiling = (cart) => { 4 | const checkShippingAddress = cart?.order.partnerShipping.id && cart?.order.partnerShipping.street 5 | return canEnterShipping(cart) && checkShippingAddress 6 | }; 7 | 8 | const canEnterPayment = (cart) => { 9 | const checkInvoiceAddress = cart?.order.partnerInvoice.id && cart?.order.partnerInvoice.street 10 | return canEnterShipping(cart) && canEnterBiling(cart) && checkInvoiceAddress; 11 | } 12 | 13 | export default async ({ app, $vsf, redirect }) => { 14 | const currentPath = app.context.route.fullPath.split('/checkout/')[1]; 15 | 16 | if (!currentPath) return; 17 | 18 | const { data } = await $vsf.$odoo.api.cartLoad(); 19 | const { cart } = data 20 | if (!cart) return; 21 | 22 | switch (currentPath) { 23 | case 'shipping': 24 | if (!canEnterShipping(cart)) { 25 | redirect('/cart'); 26 | } 27 | break; 28 | case 'billing': 29 | if (!canEnterBiling(cart)) { 30 | redirect('/cart'); 31 | } 32 | break; 33 | case 'payment': 34 | if (!canEnterPayment(cart)) { 35 | redirect('/cart'); 36 | } 37 | break; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /themeConfig.js: -------------------------------------------------------------------------------- 1 | // default configuration for links and images 2 | const defaultConfig = { 3 | home: { 4 | bannerA: { 5 | link: '/', 6 | image: { 7 | mobile: '/homepage/bannerB.webp', 8 | desktop: '/homepage/bannerF.webp' 9 | } 10 | }, 11 | bannerB: { 12 | link: '/', 13 | image: '/homepage/bannerE.webp' 14 | }, 15 | bannerC: { 16 | link: '/', 17 | image: '/homepage/bannerC.webp' 18 | }, 19 | bannerD: { 20 | link: '/', 21 | image: '/homepage/bannerG.webp' 22 | } 23 | } 24 | }; 25 | // configuration used for links and images for demo purposes 26 | const demoConfig = { 27 | home: { 28 | bannerA: { 29 | link: '/c/women/women-clothing-skirts', 30 | image: { 31 | mobile: '/homepage/bannerB.webp', 32 | desktop: '/homepage/bannerF.webp' 33 | } 34 | }, 35 | bannerB: { 36 | link: '/c/women/women-clothing-dresses', 37 | image: '/homepage/bannerE.webp' 38 | }, 39 | bannerC: { 40 | link: '/c/women/women-clothing-shirts', 41 | image: '/homepage/bannerC.webp' 42 | }, 43 | bannerD: { 44 | link: '/c/women/women-shoes-sandals', 45 | image: '/homepage/bannerG.webp' 46 | } 47 | } 48 | }; 49 | 50 | export default (() => { 51 | if (process.env.IS_DEMO) { 52 | return demoConfig; 53 | } 54 | return defaultConfig; 55 | })(); 56 | -------------------------------------------------------------------------------- /static/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /pages/Checkout/FailedPaymentResponse.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 49 | 54 | -------------------------------------------------------------------------------- /sitemap/index.ts: -------------------------------------------------------------------------------- 1 | const { integrations } = require('../middleware.config'); 2 | const graphqlBaseUrl = integrations.odoo.configuration.graphqlBaseUrl; 3 | 4 | const consola = require('consola'); 5 | const chalk = require('chalk'); 6 | const axios = require('axios'); 7 | const queries = require('../helpers/buildQueries'); 8 | 9 | const headers = { 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | 'Request-Host': integrations.odoo.configuration.baseDomain 13 | } 14 | }; 15 | 16 | const fetchProducts = async () => { 17 | return await axios.post(graphqlBaseUrl, { query: `${queries.products}` }, headers); 18 | }; 19 | 20 | const fetchCategories = async () => { 21 | return await axios.post(graphqlBaseUrl, { query: `${queries.categories}` }, headers); 22 | }; 23 | 24 | const getAppRoutes = async (): Promise> => { 25 | consola.info(chalk.bold('ODOO'), ' - Started fetch sitemap dynamic routes...'); 26 | 27 | const { data } = await fetchProducts(); 28 | const { data: categoriesData } = await fetchCategories(); 29 | 30 | consola.success(chalk.bold('ODOO'), ' - Finished fetch sitemap dynamic routes from odoo!'); 31 | 32 | return [ 33 | ...data.data.products.products.map(product => product.slug), 34 | ...categoriesData.data.categories.categories.map(category => { 35 | if (category.parent) { 36 | return category.parent.slug; 37 | } 38 | 39 | return category.slug; 40 | }) 41 | ]; 42 | }; 43 | 44 | export default getAppRoutes; 45 | -------------------------------------------------------------------------------- /layouts/blank.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 76 | -------------------------------------------------------------------------------- /composables/useUiNotification/index.ts: -------------------------------------------------------------------------------- 1 | import VueCompositionAPI, { computed, reactive } from '@vue/composition-api'; 2 | import Vue from 'vue'; 3 | 4 | // We need to register it again because of Vue instance instantiation issues 5 | Vue.use(VueCompositionAPI); 6 | interface UiNotification { 7 | type: 'danger' | 'success' | 'info'; 8 | icon?: string; 9 | persist?: boolean; 10 | id?: symbol; 11 | dismiss?: () => void; 12 | } 13 | 14 | interface Notifications { 15 | notifications: Array; 16 | } 17 | 18 | const state = reactive({ 19 | notifications: [] 20 | }); 21 | const maxVisibleNotifications = 3; 22 | const timeToLive = 3000; 23 | 24 | const useUiNotification = () : any => { 25 | const send = (notification: UiNotification) => { 26 | const id = Symbol('id'); 27 | 28 | const dismiss = () => { 29 | const index = state.notifications.findIndex( 30 | (notification) => notification.id === id 31 | ); 32 | 33 | if (index !== -1) state.notifications.splice(index, 1); 34 | }; 35 | 36 | const newNotification = { 37 | ...notification, 38 | id, 39 | dismiss 40 | }; 41 | 42 | state.notifications.push(newNotification); 43 | 44 | if (state.notifications.length > maxVisibleNotifications) 45 | state.notifications.shift(); 46 | 47 | if (!notification.persist) { 48 | setTimeout(dismiss, timeToLive); 49 | } 50 | }; 51 | 52 | return { 53 | send, 54 | notifications: computed(() => state.notifications) 55 | }; 56 | }; 57 | 58 | export default useUiNotification; 59 | -------------------------------------------------------------------------------- /custom-api/fragments/orderFragment.ts: -------------------------------------------------------------------------------- 1 | const coreProductAttribs = ` 2 | id 3 | name 4 | image 5 | description 6 | smallImage 7 | displayName 8 | slug 9 | status 10 | price 11 | combinationInfo, 12 | sku 13 | productTemplate { 14 | id 15 | name 16 | image 17 | sku 18 | slug 19 | } 20 | `; 21 | 22 | const query = ` 23 | order { 24 | id 25 | name 26 | amountTotal 27 | amountTax 28 | amountDelivery 29 | amountDiscounts 30 | dateOrder 31 | orderUrl 32 | stage 33 | websiteOrderLine { 34 | id 35 | name 36 | product { 37 | ${coreProductAttribs} 38 | } 39 | quantity 40 | priceTotal 41 | } 42 | lastTransaction{ 43 | payment{ 44 | name 45 | amount 46 | paymentReference 47 | } 48 | acquirer 49 | state 50 | amount 51 | } 52 | orderLines { 53 | id 54 | name 55 | product { 56 | ${coreProductAttribs} 57 | } 58 | quantity 59 | priceTotal 60 | } 61 | partnerInvoice { 62 | id 63 | name 64 | street 65 | phone 66 | country { 67 | id 68 | } 69 | city 70 | phone 71 | zip 72 | } 73 | partnerShipping { 74 | id 75 | name 76 | street 77 | city 78 | phone 79 | zip 80 | country { 81 | id 82 | } 83 | } 84 | shippingMethod{ 85 | id 86 | name 87 | price 88 | } 89 | } 90 | `; 91 | 92 | module.exports = query; 93 | -------------------------------------------------------------------------------- /tests/e2e/integration/addToCart.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | context('Add itens to cart from categories page', () => { 3 | beforeEach(() => { 4 | cy.intercept('POST', '/api/odoo/getCategory', { fixture: 'categoryMocksData.json' }); 5 | }); 6 | 7 | it('Should add item to cart', () => { 8 | cy.visit('http://localhost:3000'); 9 | cy.get('[data-cy=app-header-top-categories]:nth-child(1)').click(); 10 | cy.get('[data-cy=category-product-card]:nth-child(1) > [data-testid=product-link] .sf-product-card__title') 11 | .then(element => { 12 | const title = element.text(); 13 | 14 | cy.get('[data-cy=category-product-card]:nth-child(1) [data-testid=product-add-icon]').click(); 15 | 16 | cy.get('.sf-badge--number') 17 | .should('have.html', '1'); 18 | 19 | cy.get('.sf-badge--number').click(); 20 | 21 | cy.get('.sf-collected-product__title') 22 | .should('contain.html', title.replaceAll('\n', '').trim()); 23 | }); 24 | 25 | }); 26 | 27 | it('Should add item to cart twice but does not increment', () => { 28 | cy.visit('http://localhost:3000'); 29 | cy.get('[data-cy=app-header-top-categories]:nth-child(1)').click(); 30 | 31 | cy.get('[data-cy=category-product-card]:nth-child(1) [data-testid=product-add-icon]').click(); 32 | 33 | cy.get('[data-cy=category-product-card]:nth-child(1) [data-testid=product-add-icon]').click(); 34 | 35 | cy.get('.sf-badge--number') 36 | .should('have.html', '1'); 37 | 38 | cy.get('[data-testid="sf-quantity-selector input"]') 39 | .should('have.html', '1'); 40 | 41 | }); 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /assets/login/Favourites.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /composables/useProduct/index.ts: -------------------------------------------------------------------------------- 1 | import { computed } from '@nuxtjs/composition-api'; 2 | import { sharedRef, useVSFContext, Logger } from '@vue-storefront/core'; 3 | 4 | const useProduct = () : any => { 5 | // Loads context used to call API endpoint 6 | const context = useVSFContext(); 7 | 8 | // Shared ref holding the response from the API 9 | const topSellers = sharedRef(null, 'useProduct-undefined'); 10 | 11 | // Shared ref indicating whether any method is waiting for the data from the API 12 | const loading = sharedRef(false, 'useProduct-loading-undefined'); 13 | 14 | // Shared ref holding errors from the methods 15 | const error = sharedRef( 16 | { 17 | loadTopSeller: null 18 | }, 19 | 'useProduct-error-undefined' 20 | ); 21 | 22 | // Method to call an API endpoint and update `result`, `loading` and `error` properties 23 | const loadTopSeller = async (params) => { 24 | Logger.debug('useProduct/undefined/loadTopSeller', params); 25 | 26 | try { 27 | loading.value = true; 28 | // Change "odoo" to the name of the integration 29 | const { data } = await context.$odoo.api.loadTopSellers(params); 30 | 31 | topSellers.value = data.topSellers; 32 | 33 | error.value.loadTopSeller = null; 34 | } catch (err) { 35 | error.value.loadTopSeller = err; 36 | Logger.error('useProduct/undefined/loadTopSeller', err); 37 | } finally { 38 | loading.value = false; 39 | } 40 | }; 41 | 42 | return { 43 | loadTopSeller, 44 | topSellers: computed(() => topSellers.value), 45 | loading: computed(() => loading.value), 46 | error: computed(() => error.value) 47 | }; 48 | }; 49 | 50 | export default useProduct; 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | .unlighthouse 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | .lighthouseci 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env_production 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # theme 74 | _theme 75 | 76 | # Nuxt generate 77 | dist 78 | 79 | # vuepress build output 80 | .vuepress/dist 81 | 82 | # Serverless directories 83 | .serverless 84 | 85 | # IDE / Editor 86 | .idea 87 | 88 | # Service worker 89 | sw.* 90 | 91 | # Mac OSX 92 | .DS_Store 93 | 94 | # Vim swap files 95 | *.swp 96 | 97 | version 98 | -------------------------------------------------------------------------------- /components/Checkout/AdyenExternalPaymentProvider.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 59 | 60 | 73 | -------------------------------------------------------------------------------- /components/Checkout/VsfPaymentProvider.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /components/UserBillingAddress.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 53 | 54 | 62 | -------------------------------------------------------------------------------- /custom-api/customQueries.js: -------------------------------------------------------------------------------- 1 | const gql = require('graphql-tag'); 2 | const orderFragment = require('./fragments/orderFragment.ts'); 3 | import productFragment from './fragments/productFragment'; 4 | 5 | module.exports = { 6 | customCartLoad: ({ variables }) => ({ 7 | variables, 8 | query: gql` 9 | query { 10 | cart { 11 | ${orderFragment} 12 | } 13 | } 14 | ` 15 | }), 16 | confirmationPayment: ({ variables }) => ({ 17 | variables, 18 | query: gql` 19 | query { 20 | paymentConfirmation { 21 | ${orderFragment} 22 | } 23 | } 24 | ` 25 | }), 26 | 27 | customLogin: ({ variables }) => ({ 28 | variables, 29 | query: gql` 30 | mutation($email: String!, $password: String!) { 31 | login(email: $email, password: $password) { 32 | partner { 33 | id 34 | name 35 | firstName 36 | lastName 37 | street 38 | street2 39 | city 40 | state 41 | { 42 | id 43 | } 44 | country 45 | { 46 | id 47 | } 48 | email 49 | phone 50 | } 51 | } 52 | } 53 | ` 54 | }), 55 | customGetProduct: ({variables}) => ({ 56 | query: gql`query ($slug: String) { product(slug: $slug) { ${productFragment} } }`, 57 | variables 58 | }), 59 | customGetCategory: ({variables}) => ({ 60 | query: gql`query ($slug: String) { product(slug: $slug) { ${productFragment} } }`, 61 | variables 62 | }), 63 | customRegister: ({ variables }) => ({ 64 | variables, 65 | query: gql` 66 | mutation register( 67 | $email: String! 68 | $firstName: String! 69 | $lastName: String! 70 | $password: String 71 | ) { 72 | register(emailPhone: $email, firstName: $firstName, lastName: $lastName, password: $password) 73 | } 74 | ` 75 | }) 76 | }; 77 | -------------------------------------------------------------------------------- /components/UserShippingAddress.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 53 | 54 | 62 | -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | import productRoutes from './customRoutes/products.json'; 3 | import categoryRoutes from './customRoutes/categories.json'; 4 | export function getRoutes(themeDir = __dirname) { 5 | return [ 6 | { 7 | name: 'home', 8 | path: '/', 9 | component: path.resolve(themeDir, 'pages/Home.vue') 10 | }, 11 | ...productRoutes.map(item => ({ 12 | name: item.name, 13 | path: item.path, 14 | component: path.resolve(themeDir, 'pages/Product.vue') 15 | })), 16 | ...categoryRoutes.map(item => ({ 17 | name: item.name, 18 | path: item.path, 19 | component: path.resolve(themeDir, 'pages/Category.vue') 20 | })), 21 | { 22 | name: 'cart', 23 | path: '/cart', 24 | component: path.resolve(themeDir, 'pages/Cart.vue') 25 | }, 26 | { 27 | name: 'checkout', 28 | path: '/checkout', 29 | component: path.resolve(themeDir, 'pages/Checkout.vue'), 30 | children: [ 31 | { 32 | path: 'shipping', 33 | name: 'shipping', 34 | component: path.resolve(themeDir, 'pages/Checkout/Shipping.vue') 35 | }, 36 | { 37 | path: 'billing', 38 | name: 'billing', 39 | component: path.resolve(themeDir, 'pages/Checkout/Billing.vue') 40 | }, 41 | { 42 | path: 'payment', 43 | name: 'payment', 44 | component: path.resolve(themeDir, 'pages/Checkout/Payment.vue') 45 | }, 46 | { 47 | path: 'thank-you', 48 | name: 'thank-you', 49 | component: path.resolve(themeDir, 'pages/Checkout/ThankYou.vue') 50 | } 51 | ] 52 | }, 53 | { 54 | path: '/payment-fail', 55 | name: 'payment-fail', 56 | component: path.resolve(themeDir, 'pages/Checkout/FailedPaymentResponse.vue') 57 | }, 58 | { 59 | name: 'my-profile', 60 | path: '/my-account', 61 | component: path.resolve(themeDir, 'pages/MyAccount.vue') 62 | } 63 | ]; 64 | } 65 | -------------------------------------------------------------------------------- /components/Checkout/VsfShippingProvider.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 68 | 69 | 81 | -------------------------------------------------------------------------------- /hooks/buildRoutes.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const odooBaseUrl = process.env.BACKEND_BASE_URL || process.env.BASE_URL; 3 | const graphqlBaseUrl = `${odooBaseUrl}graphql/vsf`; 4 | const consola = require('consola'); 5 | const chalk = require('chalk'); 6 | const axios = require('axios'); 7 | const fsExtra = require('fs-extra'); 8 | const products = ` 9 | query { 10 | products (pageSize: 150000) { 11 | products { 12 | id 13 | slug 14 | } 15 | } 16 | } 17 | `; 18 | 19 | const categories = ` 20 | query { 21 | categories (pageSize: 10000) { 22 | categories { 23 | id 24 | name 25 | slug 26 | parent { 27 | id 28 | slug 29 | } 30 | } 31 | } 32 | } 33 | `; 34 | 35 | const headers = { headers: { 36 | 'Content-Type': 'application/json', 37 | 'resquest-host': odooBaseUrl 38 | }}; 39 | 40 | const fetchProducts = async () => { 41 | return await axios.post(graphqlBaseUrl, { query: `${products}` }, headers); 42 | }; 43 | 44 | const fetchCategories = async () => { 45 | return await axios.post(graphqlBaseUrl, { query: `${categories}` }, headers); 46 | }; 47 | 48 | const removeLastItemFromArray = (array) => { 49 | const slugs = array?.map(item =>item.slug.split('/')); 50 | const splited = slugs.map(item => ({ 51 | name: item[item.length - 1], 52 | path: item.join('/') 53 | })); 54 | return [...new Set(splited)]; 55 | }; 56 | 57 | module.exports = async () => { 58 | if (!odooBaseUrl) { 59 | consola.error(chalk.bold('ODOO'), ' - You need create a .env or set BACKEND_BASE_URL || BASE_URL '); 60 | return; 61 | } 62 | consola.info(chalk.bold('ODOO'), ' - Started fetch (product|categories) to build custom routes...'); 63 | 64 | const { data } = await fetchProducts(); 65 | const { data: categoriesData } = await fetchCategories(); 66 | 67 | await fsExtra.outputJson('customRoutes/products.json', removeLastItemFromArray(data.data.products.products)); 68 | await fsExtra.outputJson('customRoutes/categories.json', categoriesData.data.categories.categories 69 | .filter(item => item.slug && item.slug !== 'false') 70 | .map(item => ({ 71 | name: `${item.name}-${item.id}`, 72 | path: item.slug 73 | }))); 74 | 75 | consola.success(chalk.bold('ODOO'), ' - Finish build custom routes!'); 76 | }; 77 | -------------------------------------------------------------------------------- /components/MobileStoreBanner.vue: -------------------------------------------------------------------------------- 1 | 38 | 49 | 80 | -------------------------------------------------------------------------------- /composables/useUiState.ts: -------------------------------------------------------------------------------- 1 | import { reactive, computed } from '@nuxtjs/composition-api'; 2 | 3 | const state = reactive({ 4 | isCartSidebarOpen: false, 5 | isWishlistSidebarOpen: false, 6 | isLoginModalOpen: false, 7 | isNewsletterModalOpen: false, 8 | isCategoryGridView: true, 9 | isFilterSidebarOpen: false, 10 | isMobileMenuOpen: false 11 | }); 12 | 13 | const useUiState = () : any => { 14 | const isMobileMenuOpen = computed(() => state.isMobileMenuOpen); 15 | const toggleMobileMenu = () => { 16 | state.isMobileMenuOpen = !state.isMobileMenuOpen; 17 | }; 18 | 19 | const isCartSidebarOpen = computed(() => state.isCartSidebarOpen); 20 | const toggleCartSidebar = () => { 21 | if (state.isMobileMenuOpen) toggleMobileMenu(); 22 | state.isCartSidebarOpen = !state.isCartSidebarOpen; 23 | }; 24 | 25 | const isWishlistSidebarOpen = computed(() => state.isWishlistSidebarOpen); 26 | const toggleWishlistSidebar = () => { 27 | if (state.isMobileMenuOpen) toggleMobileMenu(); 28 | state.isWishlistSidebarOpen = !state.isWishlistSidebarOpen; 29 | }; 30 | 31 | const isLoginModalOpen = computed(() => state.isLoginModalOpen); 32 | const toggleLoginModal = () => { 33 | if (state.isMobileMenuOpen) toggleMobileMenu(); 34 | state.isLoginModalOpen = !state.isLoginModalOpen; 35 | }; 36 | 37 | const isNewsletterModalOpen = computed(() => state.isNewsletterModalOpen); 38 | const toggleNewsletterModal = () => { 39 | state.isNewsletterModalOpen = !state.isNewsletterModalOpen; 40 | }; 41 | 42 | const isCategoryGridView = computed(() => state.isCategoryGridView); 43 | const changeToCategoryGridView = () => { 44 | state.isCategoryGridView = true; 45 | }; 46 | const changeToCategoryListView = () => { 47 | state.isCategoryGridView = false; 48 | }; 49 | 50 | const isFilterSidebarOpen = computed(() => state.isFilterSidebarOpen); 51 | const toggleFilterSidebar = () => { 52 | state.isFilterSidebarOpen = !state.isFilterSidebarOpen; 53 | }; 54 | 55 | return { 56 | isCartSidebarOpen, 57 | isWishlistSidebarOpen, 58 | isLoginModalOpen, 59 | isNewsletterModalOpen, 60 | isCategoryGridView, 61 | isFilterSidebarOpen, 62 | isMobileMenuOpen, 63 | toggleCartSidebar, 64 | toggleWishlistSidebar, 65 | toggleLoginModal, 66 | toggleNewsletterModal, 67 | changeToCategoryGridView, 68 | changeToCategoryListView, 69 | toggleFilterSidebar, 70 | toggleMobileMenu 71 | }; 72 | }; 73 | 74 | export default useUiState; 75 | -------------------------------------------------------------------------------- /components/Notification.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 39 | 40 | 96 | -------------------------------------------------------------------------------- /customRoutes/categories.json: -------------------------------------------------------------------------------- 1 | [{"name":"WOMEN-13","path":"/category/13"},{"name":"Clothing-15","path":"/category/15"},{"name":"All-19","path":"/category/19"},{"name":"Jackets-20","path":"/category/20"},{"name":"Blazer-21","path":"/category/21"},{"name":"Tops-22","path":"/category/22"},{"name":"Shirts-23","path":"/category/23"},{"name":"T-shirts-24","path":"/category/24"},{"name":"Jeans-25","path":"/category/25"},{"name":"Trouser-26","path":"/category/26"},{"name":"Skirts-27","path":"/category/27"},{"name":"Dresses-28","path":"/category/28"},{"name":"Swimwear-29","path":"/category/29"},{"name":"Shoes-16","path":"/category/16"},{"name":"All-30","path":"/category/30"},{"name":"Sneakers-31","path":"/category/31"},{"name":"Boots-32","path":"/category/32"},{"name":"Ankle boots-33","path":"/category/33"},{"name":"Pumps-34","path":"/category/34"},{"name":"Ballerinas-35","path":"/category/35"},{"name":"Lace-up shoes-36","path":"/category/36"},{"name":"Loafers-37","path":"/category/37"},{"name":"Sandals-38","path":"/category/38"},{"name":"Winterboots-39","path":"/category/39"},{"name":"bags-17","path":"/category/17"},{"name":"All-40","path":"/category/40"},{"name":"Clutches-41","path":"/category/41"},{"name":"Shoulder Bags-42","path":"/category/42"},{"name":"Shopper-43","path":"/category/43"},{"name":"Handbag-44","path":"/category/44"},{"name":"Wallets-45","path":"/category/45"},{"name":"Bucketbag/packbag-46","path":"/category/46"},{"name":"Looks-18","path":"/category/18"},{"name":"All-47","path":"/category/47"},{"name":"MEN-14","path":"/category/14"},{"name":"Clothing-48","path":"/category/48"},{"name":"All-52","path":"/category/52"},{"name":"Jackets-53","path":"/category/53"},{"name":"Tops-54","path":"/category/54"},{"name":"Shirts-55","path":"/category/55"},{"name":"Trousers-56","path":"/category/56"},{"name":"Jeans-57","path":"/category/57"},{"name":"Blazer-58","path":"/category/58"},{"name":"Suits-59","path":"/category/59"},{"name":"T-shirts-60","path":"/category/60"},{"name":"Shoes-49","path":"/category/49"},{"name":"All-61","path":"/category/61"},{"name":"Sneakers-62","path":"/category/62"},{"name":"Boots-63","path":"/category/63"},{"name":"Lace-up shoes-64","path":"/category/64"},{"name":"Loafers-65","path":"/category/65"},{"name":"Sandals-66","path":"/category/66"},{"name":"Bags-50","path":"/category/50"},{"name":"All-67","path":"/category/67"},{"name":"Clutches-68","path":"/category/68"},{"name":"Shoulder bags-69","path":"/category/69"},{"name":"Shopper-70","path":"/category/70"},{"name":"Handbag-71","path":"/category/71"},{"name":"Wallets-72","path":"/category/72"},{"name":"Bucketbag/packbag-73","path":"/category/73"},{"name":"Looks-51","path":"/category/51"},{"name":"All-74","path":"/category/74"}] 2 | -------------------------------------------------------------------------------- /assets/styles.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | @include generate-color-variants(--disabled-grey-color, #E0E0E1); 3 | @include generate-color-variants(--disabled-grey-background, #F1F2F3); 4 | } 5 | 6 | .sf-sidebar__aside { 7 | z-index: var(--sidebar-z-index, 2 !important); 8 | } 9 | 10 | .sf-search-bar .sf-input__wrapper input{ 11 | padding: 2px 6px 3px 6px !important; 12 | } 13 | 14 | .sf-image--placeholder { 15 | opacity: 0 !important; 16 | } 17 | 18 | .notifications .sf-notification__message { 19 | margin-right: 35px; 20 | } 21 | 22 | .common_form_style .sf-input__wrapper input { 23 | padding: 25px 4px 10px 4px !important; 24 | height: 56px !important; 25 | } 26 | 27 | .common_form_style .sf-input__label { 28 | padding-left: 3px; 29 | top: 27px; 30 | } 31 | 32 | .common_form_style .sf-select__label { 33 | font-size: 18px !important; 34 | height: 0.825rem !important; 35 | } 36 | 37 | .form { 38 | --button-width: 100%; 39 | &__select { 40 | display: flex; 41 | align-items: center; 42 | --select-option-font-size: var(--font-size--lg); 43 | ::v-deep .sf-select__dropdown { 44 | font-size: var(--font-size--lg); 45 | margin: 0; 46 | color: var(--c-text); 47 | font-family: var(--font-family--secondary); 48 | font-weight: var(--font-weight--normal); 49 | } 50 | ::v-deep .sf-select__label { 51 | left: initial; 52 | } 53 | } 54 | @include for-desktop { 55 | display: flex; 56 | flex-wrap: wrap; 57 | align-items: center; 58 | --button-width: auto; 59 | } 60 | &__element { 61 | margin: 0 0 var(--spacer-lg) 0; 62 | @include for-desktop { 63 | flex: 0 0 100%; 64 | } 65 | &--half { 66 | @include for-desktop { 67 | flex: 1 1 50%; 68 | } 69 | &-even { 70 | @include for-desktop { 71 | padding: 0 0 0 var(--spacer-xl); 72 | } 73 | } 74 | } 75 | } 76 | &__action { 77 | @include for-desktop { 78 | flex: 0 0 100%; 79 | display: flex; 80 | } 81 | } 82 | &__action-button { 83 | &--secondary { 84 | @include for-desktop { 85 | order: -1; 86 | text-align: left; 87 | } 88 | } 89 | &--add-address { 90 | width: 100%; 91 | margin: 0; 92 | @include for-desktop { 93 | margin: 0 0 var(--spacer-lg) 0; 94 | width: auto; 95 | } 96 | } 97 | } 98 | &__back-button { 99 | margin: var(--spacer-xl) 0 var(--spacer-sm); 100 | &:hover { 101 | color: var(--c-white); 102 | } 103 | @include for-desktop { 104 | margin: 0 var(--spacer-xl) 0 0; 105 | } 106 | } 107 | } 108 | 109 | 110 | -------------------------------------------------------------------------------- /assets/css/account/orderHistory.scss: -------------------------------------------------------------------------------- 1 | .no-orders { 2 | &__title { 3 | margin: 0 0 var(--spacer-lg) 0; 4 | font: var(--font-weight--normal) var(--font-size--base) / 1.6 5 | var(--font-family--primary); 6 | } 7 | &__button { 8 | --button-width: 100%; 9 | @include for-desktop { 10 | --button-width: 17, 5rem; 11 | } 12 | } 13 | } 14 | .orders { 15 | @include for-desktop { 16 | &__element { 17 | &--right { 18 | --table-column-flex: 0; 19 | text-align: right; 20 | } 21 | } 22 | } 23 | } 24 | .all-orders { 25 | --button-padding: var(--spacer-base) 0; 26 | } 27 | .message { 28 | margin: 0 0 var(--spacer-xl) 0; 29 | font: var(--font-weight--light) var(--font-size--base) / 1.6 30 | var(--font-family--primary); 31 | &__link { 32 | color: var(--c-primary); 33 | font-weight: var(--font-weight--medium); 34 | font-family: var(--font-family--primary); 35 | font-size: var(--font-size--base); 36 | text-decoration: none; 37 | &:hover { 38 | color: var(--c-text); 39 | } 40 | } 41 | } 42 | .product { 43 | &__properties { 44 | margin: var(--spacer-xl) 0 0 0; 45 | } 46 | &__property, 47 | &__action { 48 | font-size: var(--font-size--sm); 49 | } 50 | &__action { 51 | color: var(--c-gray-variant); 52 | font-size: var(--font-size--sm); 53 | margin: 0 0 var(--spacer-sm) 0; 54 | &:last-child { 55 | margin: 0; 56 | } 57 | } 58 | &__qty { 59 | color: var(--c-text); 60 | } 61 | } 62 | .products { 63 | --table-column-flex: 1; 64 | &__name { 65 | margin-right: var(--spacer-sm); 66 | @include for-desktop { 67 | --table-column-flex: 2; 68 | } 69 | } 70 | } 71 | .highlighted { 72 | box-sizing: border-box; 73 | width: 100%; 74 | background-color: var(--c-light); 75 | padding: var(--spacer-sm); 76 | --property-value-font-size: var(--font-size--base); 77 | --property-name-font-size: var(--font-size--base); 78 | &:last-child { 79 | margin-bottom: 0; 80 | } 81 | ::v-deep .sf-property__name { 82 | white-space: nowrap; 83 | } 84 | ::v-deep .sf-property__value { 85 | text-align: right; 86 | } 87 | &--total { 88 | margin-bottom: var(--spacer-sm); 89 | } 90 | @include for-desktop { 91 | padding: var(--spacer-xl); 92 | --property-name-font-size: var(--font-size--lg); 93 | --property-name-font-weight: var(--font-weight--medium); 94 | --property-value-font-size: var(--font-size--lg); 95 | --property-value-font-weight: var(--font-weight--semibold); 96 | } 97 | } -------------------------------------------------------------------------------- /components/Checkout/UserBillingAddresses.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 77 | 78 | 88 | -------------------------------------------------------------------------------- /composables/useCart/index.ts: -------------------------------------------------------------------------------- 1 | import { useCart as baseUseCart } from '@vue-storefront/odoo'; 2 | import { sharedRef, useVSFContext, Logger } from '@vue-storefront/core'; 3 | import { computed } from '@nuxtjs/composition-api'; 4 | const resolvePath = (object, path, defaultValue) => path 5 | .split('.') 6 | .reduce((o, p) => o ? o[p] : defaultValue, object); 7 | 8 | const throwErrors = (errors: Array<{ message?: string }>) => { 9 | if (errors) { 10 | throw new Error(errors.map(error => error.message).join(',') || 'Some error'); 11 | } 12 | }; 13 | 14 | const useCart = () : any => { 15 | const context = useVSFContext(); 16 | const cookieIndex = context?.$odoo?.config?.app?.$config?.cart?.cookieIndex || 'orderLines'; 17 | 18 | const { cart, setCart } = baseUseCart(); 19 | const loading = sharedRef(null, 'useCart-loading'); 20 | const error = sharedRef({ 21 | load: null, 22 | clearCart: null, 23 | cartUpdateItem: null 24 | }, 'useCart-error'); 25 | 26 | const load = async ({ customQuery } = { customQuery: undefined }) => { 27 | try { 28 | loading.value = false; 29 | const { data, graphQLErrors } = await context.$odoo.api.loadCart(customQuery); 30 | 31 | throwErrors(graphQLErrors); 32 | 33 | setCart(data.cart); 34 | error.value.load = null; 35 | 36 | const cookieIndex = context?.$odoo?.config?.app?.$config?.cart?.cookieIndex || 'orderLines.length'; 37 | context.$odoo.config.app.$cookies.set('cart-size', resolvePath(cart?.value?.order, cookieIndex, 0) || 0); 38 | 39 | } catch (err) { 40 | error.value.load = err; 41 | Logger.error('useCart-load-error', err); 42 | } finally { 43 | loading.value = false; 44 | } 45 | }; 46 | 47 | const updateCartItem = async (itemId: number, quantity: number) => { 48 | try { 49 | loading.value = true; 50 | const { data, errors } = await context.$odoo.api.updateCartItem( 51 | { 52 | productId: itemId, 53 | quantity 54 | } 55 | ); 56 | 57 | throwErrors(errors); 58 | 59 | setCart(data.cartUpdateItem); 60 | const cookieIndex = context?.$odoo?.config?.app?.$config?.cart?.cookieIndex || 'orderLines.length'; 61 | context.$odoo.config.app.$cookies.set('cart-size', resolvePath(cart?.value?.order, cookieIndex, 0) || 0); 62 | 63 | error.value.cartUpdateItem = null; 64 | } catch (err) { 65 | error.value.cartUpdateItem = err; 66 | Logger.error('useCart-cartUpdateItem-error', err); 67 | } finally { 68 | loading.value = false; 69 | } 70 | }; 71 | 72 | return { 73 | cart, 74 | loading: computed(() => loading.value), 75 | load, 76 | updateCartItem, 77 | error: computed(() => error.value) 78 | }; 79 | }; 80 | 81 | export default useCart; 82 | -------------------------------------------------------------------------------- /layouts/error.vue: -------------------------------------------------------------------------------- 1 | 33 | 43 | 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-storefront/odoo-theme", 3 | "version": "1.5.2", 4 | "private": true, 5 | "scripts": { 6 | "build": "cross-var nuxt build -m --dotenv $VSF_RC", 7 | "build:local": "nuxt build -m --dotenv .env", 8 | "test:e2e": "cypress open --config-file tests/e2e/cypress.json", 9 | "test:e2e:hl": "cypress run --headless --config-file tests/e2e/cypress.json", 10 | "build:analyze": "nuxt build -a -m", 11 | "cypress:open": "cypress open", 12 | "dev": "nuxt dev", 13 | "generate": "nuxt generate", 14 | "start": "nuxt start", 15 | "update:check": "ncu", 16 | "update:update": "ncu -u", 17 | "update:routes": "node hooks/execute.js routes", 18 | "update:redirects": "node hooks/execute redirects", 19 | "test": "jest", 20 | "lhci:mobile": "lhci autorun", 21 | "lhci:desktop": "lhci autorun --collect.settings.preset=desktop" 22 | }, 23 | "dependencies": { 24 | "@adyen/adyen-web": "4.7.3", 25 | "@nuxt/image": "^0.6.1", 26 | "@nuxt/types": "latest", 27 | "@nuxtjs/amp": "^0.5.4", 28 | "@nuxtjs/axios": "^5.13.6", 29 | "@nuxtjs/device": "^2.1.0", 30 | "@nuxtjs/google-fonts": "^2.0.0", 31 | "@nuxtjs/gtm": "^2.4.0", 32 | "@nuxtjs/pwa": "^3.3.5", 33 | "@nuxtjs/redirect-module": "^0.3.1", 34 | "@nuxtjs/sitemap": "^2.4.0", 35 | "@nuxtjs/style-resources": "1.1.0", 36 | "@nuxtjs/tailwindcss": "^4.1.3", 37 | "@nuxtjs/web-vitals": "^0.1.8", 38 | "@storefront-ui/vue": "0.13.6", 39 | "@unlighthouse/nuxt": "^0.4.4", 40 | "@vue-storefront/cache": "^2.7.5", 41 | "@vue-storefront/http-cache": "^2.7.5", 42 | "@vue-storefront/middleware": "2.7.5", 43 | "@vue-storefront/nuxt": "2.7.5", 44 | "@vue-storefront/nuxt-theme": "2.7.5", 45 | "@vue-storefront/odoo": "1.5.2", 46 | "@vue-storefront/redis-cache": "^1.0.1", 47 | "@babel/plugin-proposal-private-property-in-object": "7.21.0", 48 | "cookie-universal-nuxt": "^2.1.3", 49 | "core-js": "^2.6.5", 50 | "cross-var": "^1.1.0", 51 | "express": "4.18.1", 52 | "nuxt": "^2.15.8", 53 | "nuxt-i18n": "^6.5.0", 54 | "nuxt-precompress": "^0.5.9", 55 | "nuxt-speedkit": "^2.2.0", 56 | "nuxt-winston-log": "^1.2.0", 57 | "object-hash": "^3.0.0", 58 | "redis-tag-cache": "^1.2.1", 59 | "vee-validate": "^3.2.3", 60 | "vue-scrollto": "^2.17.1" 61 | }, 62 | "devDependencies": { 63 | "@lhci/cli": "^0.9.0", 64 | "@nuxt/typescript-build": "latest", 65 | "@vue/test-utils": "^1.0.0-beta.27", 66 | "autoprefixer": "^10.2.6", 67 | "babel-jest": "^24.1.0", 68 | "cypress": "7.5.0", 69 | "lint-staged": "^11.1.2", 70 | "nuxt-vite": "^0.3.5", 71 | "postcss": "^8.3.5", 72 | "tailwindcss": "^2.1.4", 73 | "vue-cli-plugin-tailwind": "~2.0.6", 74 | "vue-jest": "^4.0.0-0" 75 | }, 76 | "resolutions": { 77 | "postcss-preset-env": "^7.0.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pages/Checkout.vue: -------------------------------------------------------------------------------- 1 | 25 | 67 | 68 | 103 | -------------------------------------------------------------------------------- /components/Checkout/UserShippingAddresses.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 75 | 76 | 101 | -------------------------------------------------------------------------------- /assets/css/account/myProfile.scss: -------------------------------------------------------------------------------- 1 | #my-account { 2 | box-sizing: border-box; 3 | @include for-desktop { 4 | max-width: 1272px; 5 | padding: 0 var(--spacer-sm); 6 | margin: 0 auto; 7 | } 8 | } 9 | .my-account { 10 | @include for-mobile { 11 | --content-pages-sidebar-category-title-font-weight: var( 12 | --font-weight--normal 13 | ); 14 | --content-pages-sidebar-category-title-margin: var(--spacer-sm) 15 | var(--spacer-sm) var(--spacer-sm) var(--spacer-base); 16 | } 17 | @include for-desktop { 18 | --content-pages-sidebar-category-title-margin: var(--spacer-xl) 0 0 0; 19 | } 20 | } 21 | .breadcrumbs { 22 | padding: var(--spacer-base) 0 var(--spacer-lg); 23 | display: none; 24 | } 25 | 26 | ::v-deep .my-account { 27 | margin-top: 40px; 28 | } 29 | 30 | ::v-deep .my-account .sf-content-pages__sidebar { 31 | max-height: 550px; 32 | height: 100%; 33 | border-radius: 14px; 34 | max-width: 374px; 35 | overflow: hidden; 36 | } 37 | 38 | ::v-deep .my-account .sf-content-pages__content { 39 | padding-left: 98px; 40 | } 41 | 42 | ::v-deep .my-account .sf-tabs__title { 43 | padding-right: 10px; 44 | font-family: var(--font-family--primary); 45 | } 46 | 47 | ::v-deep .my-account .sf-my-profile .message { 48 | color: #43464e; 49 | } 50 | 51 | ::v-deep .my-account .sf-input input { 52 | padding-left: 5px; 53 | font-weight: 300; 54 | color: #43464e; 55 | } 56 | 57 | ::v-deep .my-account .form__button { 58 | background: #32463d; 59 | color: #fff; 60 | font-family: var(--font-family--primary); 61 | max-width: 215px; 62 | width: 100%; 63 | text-align: center; 64 | border-radius: 100px; 65 | font-size: 14px; 66 | font-weight: 500; 67 | padding-top: 20px; 68 | } 69 | 70 | ::v-deep .my-account .notice { 71 | color: #000000; 72 | font-size: 14px; 73 | font-weight: 500; 74 | } 75 | 76 | ::v-deep .my-account .notice a { 77 | color: #0468db; 78 | text-decoration: underline; 79 | text-transform: uppercase; 80 | font-size: 14px; 81 | } 82 | 83 | ::v-deep .sf-content-pages__category-title { 84 | display: none; 85 | } 86 | 87 | ::v-deep .logout-btn button { 88 | color: red; 89 | } 90 | 91 | ::v-deep .sf-content-pages__title { 92 | font-family: var(--font-family--primary); 93 | font-weight: 500; 94 | font-size: 26px; 95 | } 96 | 97 | ::v-deep .sf-list .sf-list__item .sf-menu-item__label { 98 | font-family: var(--font-family--primary); 99 | font-weight: 500; 100 | font-size: 20px; 101 | } 102 | 103 | ::v-deep .sf-content-pages__sidebar div:last-child { 104 | margin-top: 100px; 105 | } 106 | 107 | ::v-deep .my-account .sf-content-pages__sidebar div:last-child span { 108 | color: #1d1f22; 109 | font-size: 14px; 110 | font-weight: 500; 111 | text-transform: uppercase; 112 | text-decoration: underline; 113 | } -------------------------------------------------------------------------------- /components/MyAccount/ShippingList.vue: -------------------------------------------------------------------------------- 1 | 2 | 51 | 95 | 100 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | 4 | services: 5 | 6 | redis: 7 | container_name: redis 8 | image: library/redis:5.0-alpine 9 | restart: unless-stopped 10 | networks: 11 | - net1 12 | volumes: 13 | - redis_data:/data 14 | 15 | nginx: 16 | env_file: .env 17 | image: nginx:latest 18 | container_name: nginx 19 | restart: unless-stopped 20 | depends_on: 21 | - odoo 22 | ports: 23 | - ${TEST_VSF_DOCKER_PORT}:${TEST_VSF_DOCKER_PORT} 24 | - ${TEST_ODOO_DOCKER_PORT}:${TEST_ODOO_DOCKER_PORT} 25 | volumes: 26 | - ./docker/nginx/templates:/etc/nginx/templates 27 | - web_root:/var/www/html 28 | networks: 29 | - net1 30 | 31 | db: 32 | env_file: .env 33 | image: postgres:12 34 | container_name: db 35 | restart: unless-stopped 36 | networks: 37 | - net1 38 | volumes: 39 | - db_home:/var/lib/postgresql/data 40 | environment: 41 | - POSTGRES_USER=odoo 42 | - POSTGRES_PASSWORD=odoo 43 | - POSTGRES_DB=v14_odoo 44 | 45 | odoo_init: 46 | env_file: .env 47 | build: 48 | context: ./docker/14.0 49 | dockerfile: Dockerfile 50 | container_name: odoo_ini 51 | working_dir: "/mnt/extra-addons" 52 | command: > 53 | bash -c "if [ ! -d "/mnt/extra-addons/graphql_vuestorefront" ]; then git clone --branch 14.0 https://github.com/odoogap/vuestorefront.git . 54 | && git submodule update --init --recursive 55 | && /entrypoint.sh odoo -d v14_odoo -i base --max-cron-threads 0 --no-http --stop-after-init -i graphql_vuestorefront 56 | && /entrypoint.sh odoo shell -d v14_odoo --max-cron-threads 0 --no-http < /start_script.py ; fi" 57 | image: odoogap 58 | restart: "no" 59 | volumes: 60 | - odoo_home:/var/lib/odoo 61 | - odoo_extra:/mnt/extra-addons 62 | - ./docker/14.0/odoo.conf:/etc/odoo/odoo.conf 63 | - ./docker/14.0/start_script.py:/start_script.py 64 | depends_on: 65 | - db 66 | networks: 67 | - net1 68 | 69 | odoo: 70 | env_file: .env 71 | build: 72 | context: ./docker/14.0 73 | dockerfile: Dockerfile 74 | container_name: odoo 75 | image: odoogap 76 | restart: unless-stopped 77 | volumes: 78 | - odoo_home:/var/lib/odoo 79 | - odoo_extra:/mnt/extra-addons 80 | - ./docker/14.0/odoo.conf:/etc/odoo/odoo.conf 81 | depends_on: 82 | - db 83 | networks: 84 | - net1 85 | 86 | vsf: 87 | env_file: .env 88 | environment: 89 | BASE_URL: http://localhost:8069/ 90 | BACKEND_BASE_URL: http://odoo:8069/ 91 | REDIS_HOST: redis 92 | REDIS_PORT: 6379 93 | container_name: vsf 94 | image: vsf2 95 | restart: unless-stopped 96 | depends_on: 97 | - redis 98 | - odoo 99 | networks: 100 | - net1 101 | 102 | 103 | volumes: 104 | db_home: 105 | external: false 106 | odoo_home: 107 | external: false 108 | odoo_extra: 109 | external: False 110 | web_root: 111 | external: false 112 | redis_data: 113 | external: false 114 | 115 | networks: 116 | net1: 117 | name: net1 118 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 52 | 53 | 119 | -------------------------------------------------------------------------------- /components/LocaleSelector.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 78 | 79 | 123 | -------------------------------------------------------------------------------- /components/NewsletterModal.vue: -------------------------------------------------------------------------------- 1 | 49 | 102 | 103 | 127 | -------------------------------------------------------------------------------- /components/BottomNavigation.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 114 | 127 | -------------------------------------------------------------------------------- /components/MyAccount/NewsletterForm.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 119 | 120 | 123 | -------------------------------------------------------------------------------- /components/RelatedProducts.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 116 | 117 | 132 | -------------------------------------------------------------------------------- /components/MyAccount/PasswordResetForm.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 113 | 114 | 145 | -------------------------------------------------------------------------------- /components/InstagramFeed.vue: -------------------------------------------------------------------------------- 1 | 87 | 97 | 148 | -------------------------------------------------------------------------------- /assets/css/category.scss: -------------------------------------------------------------------------------- 1 | #category { 2 | box-sizing: border-box; 3 | @include for-desktop { 4 | max-width: 1240px; 5 | margin: 0 auto; 6 | } 7 | } 8 | .main { 9 | &.section { 10 | padding: var(--spacer-xs); 11 | @include for-desktop { 12 | padding: 0; 13 | } 14 | } 15 | } 16 | .breadcrumbs { 17 | margin: var(--spacer-base) auto var(--spacer-lg); 18 | text-transform: capitalize; 19 | } 20 | 21 | .sort-by { 22 | flex: unset; 23 | width: 11.875rem; 24 | } 25 | .main { 26 | display: flex; 27 | } 28 | 29 | .sidebar { 30 | flex: 0 0 15%; 31 | padding: var(--spacer-sm); 32 | border: 1px solid var(--c-light); 33 | border-width: 0 1px 0 0; 34 | } 35 | .list { 36 | --menu-item-font-size: var(--font-size--sm); 37 | &__item { 38 | &:not(:last-of-type) { 39 | --list-item-margin: 0 0 var(--spacer-sm) 0; 40 | } 41 | 42 | .nuxt-link-exact-active { 43 | text-decoration: underline; 44 | } 45 | } 46 | } 47 | .products { 48 | box-sizing: border-box; 49 | flex: 1; 50 | margin: 0; 51 | &__grid { 52 | justify-content: space-between; 53 | @include for-desktop { 54 | justify-content: flex-start; 55 | } 56 | } 57 | &__grid, 58 | &__list { 59 | display: flex; 60 | flex-wrap: wrap; 61 | } 62 | &__product-card { 63 | --product-card-title-margin: var(--spacer-base) 0 0 0; 64 | --product-card-title-font-weight: var(--font-weight--medium); 65 | --product-card-title-margin: var(--spacer-xs) 0 0 0; 66 | flex: 1 1 50%; 67 | @include for-desktop { 68 | --product-card-title-font-weight: var(--font-weight--normal); 69 | --product-card-add-button-bottom: var(--spacer-base); 70 | --product-card-title-margin: var(--spacer-sm) 0 0 0; 71 | } 72 | } 73 | &__product-card-horizontal { 74 | flex: 0 0 100%; 75 | } 76 | &__slide-enter { 77 | opacity: 0; 78 | transform: scale(0.5); 79 | } 80 | &__slide-enter-active { 81 | transition: all 0.2s ease; 82 | transition-delay: calc(0.1s * var(--index)); 83 | } 84 | @include for-desktop { 85 | &__grid { 86 | margin: var(--spacer-sm) 0 0 var(--spacer-sm); 87 | } 88 | &__pagination { 89 | display: flex; 90 | justify-content: flex-start; 91 | margin: var(--spacer-xl) 0 0 0; 92 | } 93 | &__product-card-horizontal { 94 | margin: var(--spacer-lg) 0; 95 | } 96 | &__product-card { 97 | flex: 1 1 25%; 98 | } 99 | &__list { 100 | margin: 0 0 0 var(--spacer-sm); 101 | } 102 | } 103 | &__show-on-page { 104 | display: flex; 105 | justify-content: flex-end; 106 | align-items: baseline; 107 | &__label { 108 | font-family: var(--font-family--secondary); 109 | font-size: var(--font-size--sm); 110 | } 111 | } 112 | } 113 | .loading { 114 | margin: var(--spacer-3xl) auto; 115 | @include for-desktop { 116 | margin-top: 6.25rem; 117 | } 118 | } 119 | ::v-deep .sf-sidebar__aside { 120 | --sidebar-z-index: 3; 121 | } 122 | 123 | .before-results { 124 | box-sizing: border-box; 125 | padding: var(--spacer-lg) var(--spacer-sm) var(--spacer-2xl); 126 | width: 100%; 127 | text-align: center; 128 | @include for-desktop { 129 | padding: 0; 130 | } 131 | &__picture { 132 | --image-width: 230px; 133 | margin-top: var(--spacer-2xl); 134 | @include for-desktop { 135 | --image-width: 18.75rem; 136 | margin-top: var(--spacer-base); 137 | } 138 | } 139 | &__paragraph { 140 | font-family: var(--font-family--primary); 141 | font-weight: var(--font-weight--normal); 142 | font-size: var(--font-size--base); 143 | color: var(--c-text-muted); 144 | margin: 0; 145 | @include for-desktop { 146 | font-size: var(--font-size--lg); 147 | } 148 | &:first-of-type { 149 | margin: var(--spacer-xl) auto var(--spacer-xs); 150 | } 151 | } 152 | &__button { 153 | margin: var(--spacer-xl) auto; 154 | width: 100%; 155 | } 156 | } -------------------------------------------------------------------------------- /components/Core/Atoms/OdooButton.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 115 | 116 | 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Vue Storefront 3 |
4 | 5 | ### Stay connected 6 | 7 | [![GitHub Repo stars](https://img.shields.io/github/stars/vuestorefront/vue-storefront?style=social)](https://github.com/vuestorefront/vue-storefront) 8 | [![Twitter Follow](https://img.shields.io/twitter/follow/vuestorefront?style=social)](https://twitter.com/vuestorefront) 9 | [![YouTube Channel Subscribers](https://img.shields.io/youtube/channel/subscribers/UCkm1F3Cglty3CE1QwKQUhhg?style=social)](https://www.youtube.com/c/VueStorefront) 10 | [![Discord](https://img.shields.io/discord/770285988244750366?label=join%20discord&logo=Discord&logoColor=white)](https://discord.vuestorefront.io) 11 | 12 | ## Vue Storefront 2 integration with Odoo 13 | 14 | This project is a Odoo integration with [Vue Storefront 2](https://github.com/vuestorefront/vue-storefront/). 15 | This integration is being developed and maintained by [ODOOGAP / PROMPTEQUATION](https://www.odoogap.com/) ❤️ 16 | 17 | Check our [demo](https://vsf.labs.odoogap.com/) server (it's a dev server so could be down sometimes) 18 | 19 | 20 | ## How to start? 21 | 22 | 1. Copy .env.example to .env 23 | 2. DOT NOT COMMIT .env file 24 | 25 | 26 | ```sh 27 | # install dependencies 28 | yarn install 29 | 30 | # generate routes and redirects from ODOO 31 | yarn update:routes 32 | yarn update:redirects 33 | 34 | # serve with hot reload at localhost:3000 35 | yarn dev 36 | 37 | # build for production and launch server 38 | yarn build 39 | # build local 40 | yarn build:local 41 | ``` 42 | 43 | or use docker-compose 44 | 45 | ```bash 46 | docker-compose up --build -d 47 | # you might need to 48 | docker-compose restart odoo nginx 49 | ``` 50 | 51 | ## middleware.config.js 52 | 53 | redisClient will enable/disable cache redis on API level (different from SSR cache) 54 | 55 | 56 | 57 | Want to contribute? Ping us on `odoo` channel on [our Discord](https://discord.vuestorefront.io) or email us at info (at) odoogap.com! 58 | 59 | ## Resources 60 | 61 | - [Vue Storefront Documentation](https://docs.vuestorefront.io/v2/) 62 | - [Odoo integration Documentation](https://docs.vuestorefront.io/odoo) 63 | - [Odoo integration](https://github.com/vuestorefront/odoo) 64 | 65 | ## Support 66 | 67 | If you have any questions about this integration we will be happy to answer them on `odoo` channel on [our Discord](discord.vuestorefront.io). 68 | 69 | ## Credits 70 | 71 | ### Authors: 72 | 73 | - ODOOGAP / PROMPTEQUATION 74 | 75 | ## Contributors ✨ 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |

cpintofonseca

💻

SDMonteiro

💻

brunoodoogap

💻

Diogo Duarte

💻

Leonardo Ribeiro

💻
89 | 90 | 91 | 92 | 93 | 94 | 95 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 96 | -------------------------------------------------------------------------------- /components/MobileMenuSidebar.vue: -------------------------------------------------------------------------------- 1 | 58 | 154 | 155 | 158 | -------------------------------------------------------------------------------- /components/MyAccount/ProfileUpdateForm.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 135 | 166 | -------------------------------------------------------------------------------- /assets/css/product.scss: -------------------------------------------------------------------------------- 1 | #product { 2 | box-sizing: border-box; 3 | @include for-desktop { 4 | max-width: 1272px; 5 | margin: 0 auto; 6 | } 7 | } 8 | .product { 9 | @include for-desktop { 10 | display: grid; 11 | grid-template-columns: repeat(12, minmax(0, 1fr)); 12 | } 13 | 14 | &__info { 15 | margin: var(--spacer-sm) auto; 16 | @include for-desktop { 17 | max-width: 32.625rem; 18 | margin: 0 0 0 7.5rem; 19 | } 20 | } 21 | &__header { 22 | --heading-title-color: var(--c-link); 23 | --heading-title-font-weight: var(--font-weight--bold); 24 | --heading-padding: 0; 25 | margin: 0 var(--spacer-sm); 26 | display: flex; 27 | justify-content: space-between; 28 | @include for-desktop { 29 | --heading-title-font-weight: var(--font-weight--semibold); 30 | margin: 0 auto; 31 | } 32 | } 33 | &__drag-icon { 34 | animation: moveicon 1s ease-in-out infinite; 35 | } 36 | &__price-and-rating { 37 | margin: 0 var(--spacer-sm) var(--spacer-base); 38 | align-items: center; 39 | @include for-desktop { 40 | display: flex; 41 | justify-content: space-between; 42 | margin: var(--spacer-sm) 0 var(--spacer-lg) 0; 43 | } 44 | } 45 | &__rating { 46 | display: flex; 47 | align-items: center; 48 | justify-content: flex-end; 49 | margin: var(--spacer-xs) 0 var(--spacer-xs); 50 | } 51 | &__count { 52 | @include font( 53 | --count-font, 54 | var(--font-weight--normal), 55 | var(--font-size--sm), 56 | 1.4, 57 | var(--font-family--secondary) 58 | ); 59 | color: var(--c-text); 60 | text-decoration: none; 61 | margin: 0 0 0 var(--spacer-xs); 62 | } 63 | &__description { 64 | @include font( 65 | --product-description-font, 66 | var(--font-weight--light), 67 | var(--font-size--base), 68 | 1.6, 69 | var(--font-family--primary) 70 | ); 71 | } 72 | &__select-size { 73 | margin: 0 var(--spacer-sm); 74 | @include for-desktop { 75 | margin: 0; 76 | } 77 | } 78 | &__colors { 79 | @include font( 80 | --product-color-font, 81 | var(--font-weight--normal), 82 | var(--font-size--lg), 83 | 1.6, 84 | var(--font-family--secondary) 85 | ); 86 | display: flex; 87 | align-items: center; 88 | margin-top: var(--spacer-xl); 89 | } 90 | &__radio-label { 91 | @include font( 92 | --product-color-font, 93 | var(--font-weight--normal), 94 | var(--font-size--lg), 95 | 1.6, 96 | var(--font-family--secondary) 97 | ); 98 | margin: 0 var(--spacer-lg) 0 0; 99 | } 100 | &__color-label { 101 | margin: 0 var(--spacer-lg) 0 0; 102 | } 103 | &__color { 104 | margin: 0 var(--spacer-2xs); 105 | } 106 | &__add-to-cart { 107 | margin: var(--spacer-base) var(--spacer-sm) 0; 108 | @include for-desktop { 109 | margin-top: var(--spacer-2xl); 110 | } 111 | } 112 | &__guide, 113 | &__compare, 114 | &__save { 115 | display: block; 116 | margin: var(--spacer-xl) 0 var(--spacer-base) auto; 117 | } 118 | &__compare { 119 | margin-top: 0; 120 | } 121 | &__tabs { 122 | margin: var(--spacer-lg) auto var(--spacer-2xl); 123 | --tabs-title-font-size: var(--font-size--lg); 124 | @include for-desktop { 125 | margin-top: var(--spacer-2xl); 126 | } 127 | } 128 | &__property { 129 | margin: var(--spacer-base) 0; 130 | &__button { 131 | --button-font-size: var(--font-size--base); 132 | } 133 | } 134 | &__review { 135 | padding-bottom: 24px; 136 | border-bottom: var(--c-light) solid 1px; 137 | margin-bottom: var(--spacer-base); 138 | } 139 | &__additional-info { 140 | color: var(--c-link); 141 | @include font( 142 | --additional-info-font, 143 | var(--font-weight--light), 144 | var(--font-size--sm), 145 | 1.6, 146 | var(--font-family--primary) 147 | ); 148 | &__title { 149 | font-weight: var(--font-weight--normal); 150 | font-size: var(--font-size--base); 151 | margin: 0 0 var(--spacer-sm); 152 | &:not(:first-child) { 153 | margin-top: 3.5rem; 154 | } 155 | } 156 | &__paragraph { 157 | margin: 0; 158 | } 159 | } 160 | &__gallery { 161 | flex: 1; 162 | } 163 | } 164 | 165 | .breadcrumbs { 166 | margin: var(--spacer-base) auto var(--spacer-lg); 167 | text-transform: capitalize; 168 | } 169 | @keyframes moveicon { 170 | 0% { 171 | transform: translate3d(0, 0, 0); 172 | } 173 | 50% { 174 | transform: translate3d(0, 30%, 0); 175 | } 176 | 100% { 177 | transform: translate3d(0, 0, 0); 178 | } 179 | } -------------------------------------------------------------------------------- /components/Checkout/AdyenDirectPaymentProvider.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 171 | 172 | 185 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const isDev = process.env.NODE_ENV !== 'production'; 2 | 3 | module.exports = { 4 | purge: { 5 | enabled: !isDev, 6 | content: [ 7 | 'components/**/*.vue', 8 | 'layouts/**/*.vue', 9 | 'pages/**/*.vue', 10 | 'plugins/**/*.js', 11 | 'nuxt.config.js' 12 | ], 13 | options: { 14 | whitelist: [ 15 | ], 16 | whitelistPatternsChildren: [ 17 | ] 18 | } 19 | }, 20 | important: true, 21 | theme: { 22 | extend: { 23 | fontSize: { 24 | 'sf-xs': 'var(--font-size--xs)', // 12px 25 | 'sf-sm': 'var(--font-size--sm)', // 14px 26 | 'sf-base': 'var(--font-size--base)', // 16px 27 | 'sf-lg': 'var(--font-size--lg)' // 18px 28 | }, 29 | fontWeight: { 30 | 'sf-light': 'var(--font-weight--light)', // 300 31 | 'sf-normal': 'var(--font-weight--normal)', // 400 32 | 'sf-medium': 'var(--font-weight--medium)', // 500 33 | 'sf-semibold': 'var(--font-weight--semibold)', // 600 34 | 'sf-bold': 'var(--font-weight--bold)' // 700 35 | }, 36 | colors: { 37 | 'sf-c-black': 'var(--c-black)', // #1d1f22 38 | 'sf-c-black-base': 'var(--c-black-base)', // #1d1f22 39 | 'sf-c-black-lighten': 'var(--c-black-lighten)', // #292c30 40 | 'sf-c-black-darken': 'var( --c-black-darken)', // #111214 41 | 'sf-c-white': 'var(--c-white)', // #ffffff 42 | 'sf-c-body': 'var(--c-body)', // #ffffff 43 | 'sf-c-text': 'var(--c-text)', // #1d1f22 44 | 'sf-c-text-muted': 'var(--c-text-muted)', // #72757E 45 | 'sf-c-text-disabled': 'var(--c-text-disabled)', // #e0e0e1 46 | 'sf-c-link': 'var(--c-link)', // #43464E 47 | 'sf-c-link-hover': 'var(--c-link-hover)', // // #1d1f22 48 | 'sf-c-primary': 'var(--c-primary)', // #5ece7b 49 | 'sf-c-primary-base': 'var(--c-primary-base)', // #5ece7b 50 | 'sf-c-primary-lighten': 'var(--c-primary-lighten)', // #72d48b 51 | 'sf-c-primary-darken': 'var(--c-primary-darken)', // #4ac86b 52 | 'sf-c-primary-variant': 'var(--c-primary-variant)', // #9ee2b0 53 | 'sf-c-on-primary': 'var(--c-on-primary)', // #ffffff 54 | 'sf-c-secondary': 'var( --c-secondary)', // #1d1f22 55 | 'sf-c-secondary-base': 'var(--c-secondary-base)', // #1d1f22 56 | 'sf-c-secondary-lighten': 'var(--c-secondary-lighten)', // #292c30 57 | 'sf-c-secondary-darken': 'var(--c-secondary-darken)', // #111214 58 | 'sf-c-secondary-variant': 'var(--c-secondary-variant)', // #43464E 59 | 'sf-c-on-secondary': 'var(--c-on-secondary)', // #ffffff 60 | 'sf-c-light': 'var(--c-light)', // #f1f2f3 61 | 'sf-c-light-base': 'var(--c-light-base)', // #f1f2f3 62 | 'sf-c-light-lighten': 'var(--c-light-lighten)', // #ffffff 63 | 'sf-c-light-darken': 'var(--c-light-darken)', // #e3e5e7 64 | 'sf-c-light-variant': 'var(--c-light-variant)', // #ffffff 65 | 'sf-c-on-light': 'var(--c-on-light)', // #1d1f22 66 | 'sf-c-gray': 'var(--c-gray)', // #72757E 67 | 'sf-c-gray-base': 'var(--c-gray-base)', // #72757E 68 | 'sf-c-gray-lighten': 'var(--c-gray-lighten)', // #7f828b 69 | 'sf-c-gray-darken': 'var(--c-gray-darken)', // #666971 70 | 'sf-c-gray-variant': 'var(--c-gray-variant)', // #8D8F9A 71 | 'sf-c-on-gray': 'var(--c-on-gray)', // #1d1f22 72 | 'sf-c-dark': 'var(--c-dark)', // #1d1f22 73 | 'sf-c-dark-base': 'var(--c-dark-base)', // #1d1f22 74 | 'sf-c-dark-lighten': 'var(--c-dark-lighten)', // #292c30 75 | 'sf-c-dark-darken': 'var(--c-dark-darken)', // #111214 76 | 'sf-c-dark-variant': 'var(--c-dark-variant)', // #43464E 77 | 'sf-c-on-dark': 'var(--c-on-dark)', // #ffffff 78 | 'sf-c-info': 'var(--c-info)', // #0468DB 79 | 'sf-c-info-base': 'var(--c-info-base)', // #0468DB 80 | 'sf-c-info-lighten': 'var(--c-info-lighten)', // #0474f4 81 | 'sf-c-info-darken': 'var(--c-info-darken)', // #045cc2 82 | 'sf-c-info-variant': 'var(--c-info-variant)', // #e1f4fe 83 | 'sf-c-on-info': 'var(--c-on-info)', // #ffffff 84 | 'sf-c-success': 'var(--c-success)', // #5ece7b 85 | 'sf-c-success-base': 'var(--c-success-base)', // #5ece7b 86 | 'sf-c-success-lighten': 'var(--c-success-lighten)', // #72d48b 87 | 'sf-c-success-darken': 'var(--c-success-darken)', // #4ac86b 88 | 'sf-c-success-variant': 'var(--c-success-variant)', // #9ee2b0 89 | 'sf-c-on-success': 'var(--c-on-success)', // #ffffff 90 | 'sf-c-warning': 'var(--c-warning)', // #ecc713 91 | 'sf-c-warning-base': 'var(--c-warning-base)', // #ecc713 92 | 'sf-c-warning-lighten': 'var(--c-warning-lighten)', // #eecd2b 93 | 'sf-c-warning-darken': 'var(--c-warning-darken)', // #d4b311 94 | 'sf-c-warning-variant': 'var(--c-warning-variant)', // #f6e389 95 | 'sf-c-on-warning': 'var(--c-on-warning)', // #ffffff 96 | 'sf-c-danger': 'var(--c-danger)', // #d12727 97 | 'sf-c-danger-base': 'var(--c-danger-base)', // #d12727 98 | 'sf-c-danger-lighten': 'var(--c-danger-lighten)', // #da3838 99 | 'sf-c-danger-darken': 'var(--c-danger-darken)', // #bc2323 100 | 'sf-c-danger-variant': 'var(--c-danger-variant)', // #fcede8 101 | 'sf-c-on-danger': 'var(--c-on-danger)' // #ffffff 102 | }, 103 | spacing: { 104 | 'sf-2xs': 'var(--spacer-2xs)', // 4px 105 | 'sf-xs': 'var( --spacer-xs)', // 8px 106 | 'sf-sm': 'var(--spacer-sm)', // 16px 107 | 'sf-base': 'var(--spacer-base)', // 24px 108 | 'sf-lg': 'var(--spacer-lg)', // 32px 109 | 'sf-xl': 'var(--spacer-xl)', // 40px 110 | 'sf-2xl': 'var(--spacer-2xl)', // 80px 111 | 'sf-3xl': 'var(--spacer-3xl)' // 160px 112 | }, 113 | fontFamily: { 114 | 'sf-primary': 'var(--font-family--primary)', // "Roboto", serif 115 | 'sf-secondary': 'var(--font-family--secondary)' // "Raleway", sans-serif 116 | } 117 | } 118 | }, 119 | variants: {}, 120 | plugins: [] 121 | }; 122 | -------------------------------------------------------------------------------- /pages/MyAccount.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 195 | 196 | 199 | -------------------------------------------------------------------------------- /components/Checkout/CartPreview.vue: -------------------------------------------------------------------------------- 1 | 77 | 164 | 165 | 226 | -------------------------------------------------------------------------------- /components/MyAccount/OrderHistory.vue: -------------------------------------------------------------------------------- 1 | 114 | 115 | 196 | 197 | 200 | -------------------------------------------------------------------------------- /composables/useUiHelpers/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /* eslint-disable camelcase */ 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-nocheck 5 | 6 | import { useRoute, useRouter } from '@nuxtjs/composition-api'; 7 | import { Category } from '@vue-storefront/odoo-api/server'; 8 | import hash from 'object-hash'; 9 | import { facetGetters, useFacet } from '@vue-storefront/odoo'; 10 | const queryParamsNotFilters = ['page', 'sort', 'itemsPerPage']; 11 | 12 | const useUiHelpers = (): any => { 13 | const route = useRoute(); 14 | const router = useRouter(); 15 | const { params, query, path } = route.value; 16 | const localePrefixes = ['/en', '/de', '/ru']; 17 | 18 | const pathToSlug = () : string => { 19 | for (const localePrefix of localePrefixes) { 20 | if (path.startsWith(localePrefix)) { 21 | return path.replace(localePrefix, ''); 22 | } 23 | } 24 | return path; 25 | }; 26 | 27 | 28 | 29 | const getAttributeValues = (filterKey, value) => { 30 | const { result } = useFacet(); 31 | const facets = [ 32 | { 33 | id: null, 34 | label: 'Price', 35 | type: 'price' 36 | }, 37 | ...facetGetters.getGrouped(result?.value, ['color', 'size']) 38 | ]; 39 | const attribute = facets?.find(item => { 40 | return item.label == filterKey 41 | }) 42 | let option = {} 43 | if (attribute) { 44 | option = attribute?.options.find(item => { 45 | return Number(item.value) === Number(value.slice(0,2)) 46 | }) 47 | } 48 | return option; 49 | } 50 | 51 | const getFacetsFromURL = (): ParamsFromUrl => { 52 | const filters: string[] = []; 53 | if (query) { 54 | Object.keys(query).forEach((filterKey) => { 55 | if (![...queryParamsNotFilters, 'price'].includes(filterKey)) { 56 | if (query[filterKey].includes(',')) { 57 | query[filterKey]?.split(',').forEach(label => { 58 | const getProperAttribute = getAttributeValues(filterKey, label?.split('-')[0]) 59 | filters.push(getProperAttribute?.id); 60 | }) 61 | } else { 62 | const label = query[filterKey]?.split(',')[0]; 63 | const getProperAttribute = getAttributeValues(filterKey, label); 64 | filters.push(getProperAttribute?.id); 65 | } 66 | } 67 | }); 68 | } 69 | 70 | const price = query?.price?.split('-'); 71 | 72 | const pageSize = query.itemsPerPage ? parseInt(query.itemsPerPage) : 12; 73 | const sort = query?.sort?.split(',') || []; 74 | const page = query?.page || 1; 75 | 76 | const productFilters = { 77 | minPrice: Number(price?.[0]) || null, 78 | maxPrice: Number(price?.[1]) || null, 79 | attribValues: filters, 80 | categorySlug: path === '/' ? null : pathToSlug() 81 | }; 82 | const filtersForHash = { 83 | ...productFilters, 84 | pageSize, 85 | price, 86 | page, 87 | sort 88 | } 89 | 90 | return { 91 | fetchCategory: true, 92 | categoryParams: { 93 | slug: path === '/' ? null : pathToSlug(), 94 | cacheKey: `API-C${route.value.path}` 95 | }, 96 | productParams: { 97 | pageSize, 98 | currentPage: parseInt(page), 99 | cacheKey: `API-P${hash(filtersForHash, { algorithm: 'md5' })}`, 100 | search: '', 101 | sort: { [sort[0]]: sort[1] }, 102 | filter: productFilters 103 | } 104 | }; 105 | }; 106 | 107 | const getCatLink = (category: Category): string => { 108 | return category.slug; 109 | }; 110 | 111 | const getCatLinkForSearch = (category: Category): string => { 112 | return category.slug; 113 | }; 114 | 115 | const changeSorting = (sort: string) => { 116 | router.push({ query: { ...query, sort } }); 117 | }; 118 | 119 | const facetsFromUrlToFilter = () => { 120 | const formatedFilters = []; 121 | Object.keys(query).forEach((label) => { 122 | if (queryParamsNotFilters.includes(label)) return; 123 | 124 | const valueList = query[label].split(','); 125 | 126 | valueList.forEach((value) => { 127 | if(label === 'price') { 128 | const item = { 129 | filterName: label, 130 | label: `${value.slice(0, 2)}`, 131 | id: value 132 | }; 133 | formatedFilters.push(item); 134 | } else { 135 | const newVal = value?.split('-') 136 | const item = { 137 | filterName: label, 138 | label: `${newVal[1] ?? newVal[0]}`, 139 | id: `${newVal[0]}` 140 | }; 141 | formatedFilters.push(item); 142 | } 143 | }); 144 | }); 145 | 146 | return formatedFilters; 147 | }; 148 | 149 | const changeFilters = (filters) => { 150 | const formatedFilters = {}; 151 | filters.forEach((element) => { 152 | if(element.filterName == "Size") { 153 | if (formatedFilters[element.filterName]) { 154 | formatedFilters[element.filterName] += `,${element.id}-${element.label}`; 155 | return; 156 | } 157 | formatedFilters[element.filterName] = `${element.id}-${element.label}`; 158 | } else { 159 | if (formatedFilters[element.filterName]) { 160 | formatedFilters[element.filterName] += `,${element.id}`; 161 | return; 162 | } 163 | formatedFilters[element.filterName] = `${element.id}`; 164 | } 165 | }); 166 | 167 | let allQuery = {}; 168 | if (filters.length > 0) { 169 | allQuery = { ...query, ...formatedFilters }; 170 | } else { 171 | allQuery = { ...formatedFilters }; 172 | if (query.itemsPerPage) { 173 | allQuery = { itemsPerPage: query.itemsPerPage }; 174 | } 175 | } 176 | 177 | delete allQuery.page 178 | 179 | router.push({ query: allQuery }); 180 | }; 181 | 182 | const changeItemsPerPage = (itemsPerPage) => { 183 | delete query.page; 184 | router.push({ query: { ...query, itemsPerPage } }); 185 | }; 186 | 187 | const changeSearchTerm = (term: string) => term; 188 | 189 | const isFacetColor = (facet): boolean => { 190 | return facet.type === 'color'; 191 | }; 192 | 193 | const isFacetPrice = (facet): boolean => { 194 | return facet.type === 'price'; 195 | }; 196 | 197 | const isFacetCheckbox = (facet): boolean => { 198 | console.warn('[VSF] please implement useUiHelpers.isFacetCheckbox.'); 199 | 200 | return false; 201 | }; 202 | 203 | const getComponentProviderByName = (provider: string): string => { 204 | if (!provider) throw new Error('Provider without provider'); 205 | 206 | const upperName = provider.toLocaleUpperCase(); 207 | 208 | if (upperName === 'ADYEN_OG') { 209 | return 'AdyenExternalPaymentProvider'; 210 | } 211 | 212 | if (upperName === 'ADYEN') { 213 | return 'AdyenDirectPaymentProvider'; 214 | } 215 | 216 | if (upperName.includes('WIRE')) { 217 | return 'WireTransferPaymentProvider'; 218 | } 219 | 220 | throw new Error(`Provider ${name} not implemented!`); 221 | }; 222 | 223 | return { 224 | getFacetsFromURL, 225 | getCatLink, 226 | getCatLinkForSearch, 227 | changeSorting, 228 | changeFilters, 229 | changeItemsPerPage, 230 | changeSearchTerm, 231 | isFacetColor, 232 | isFacetPrice, 233 | isFacetCheckbox, 234 | facetsFromUrlToFilter, 235 | getComponentProviderByName, 236 | pathToSlug 237 | }; 238 | }; 239 | 240 | export default useUiHelpers; 241 | -------------------------------------------------------------------------------- /components/MyAccount/ShippingAddressForm.vue: -------------------------------------------------------------------------------- 1 | 167 | 168 | 259 | --------------------------------------------------------------------------------