diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a86cb7102..f7c7f7800 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,6 +5,7 @@ Closes: #ID ## Describe your changes ## Checklist before requesting a review + - [ ] My code follows the code style of this project. - [ ] I have read the **CONTRIBUTING** document. - [ ] I have added tests to cover my changes. diff --git a/.github/workflows/build-and-deploy.yaml b/.github/workflows/build-and-deploy.yaml index 39046fa35..eaa4a6a70 100644 --- a/.github/workflows/build-and-deploy.yaml +++ b/.github/workflows/build-and-deploy.yaml @@ -1,9 +1,10 @@ name: Deploy on: - push: - branches: - - main - - force/* + # push: + # branches: + # - main + # - force/* + workflow_dispatch: jobs: configure: diff --git a/.github/workflows/conventional-pr-name.yml b/.github/workflows/conventional-pr-name.yml index ff9de959b..7271dc1b7 100644 --- a/.github/workflows/conventional-pr-name.yml +++ b/.github/workflows/conventional-pr-name.yml @@ -15,6 +15,9 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Enable corepack + run: corepack enable + - name: Setup node uses: actions/setup-node@v3 with: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 25373e958..c8fbca5ed 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,6 +20,9 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Enable corepack + run: corepack enable + - name: Setup node uses: actions/setup-node@v3 with: @@ -44,8 +47,8 @@ jobs: - name: Install Dependencies run: yarn --frozen-lockfile - - name: Build packages - run: yarn build + - name: Run prettier + run: yarn format - name: Run linter run: yarn lint diff --git a/.github/workflows/run-lhci-test.yml b/.github/workflows/run-lhci-test.yml index 4c66e53db..5afd655f3 100644 --- a/.github/workflows/run-lhci-test.yml +++ b/.github/workflows/run-lhci-test.yml @@ -19,6 +19,9 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Enable corepack + run: corepack enable + - name: Setup node uses: actions/setup-node@v3 with: diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/tests.yaml similarity index 69% rename from .github/workflows/e2e-tests.yaml rename to .github/workflows/tests.yaml index 7d9b6052f..646487624 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,4 +1,4 @@ -name: e2e-tests +name: tests on: workflow_dispatch: @@ -9,13 +9,16 @@ permissions: packages: read jobs: - e2e-tests: + tests: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 + - name: Enable corepack + run: corepack enable + - name: Setup node uses: actions/setup-node@v3 with: @@ -36,24 +39,30 @@ jobs: yarn config set npmScopes.plentymarkets.npmAuthToken $NODE_AUTH_TOKEN env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CYPRESS_BASE_URL: 'http://${{ env.ip_address }}:3000' - PAYPAL_EMAIL: ${{ secrets.PAYPAL_EMAIL }} - PAYPAL_PASSWORD: ${{ secrets.PAYPAL_PASSWORD }} - - - name: Setup middleware env - run: | - touch apps/server/.env - echo API_ENDPOINT=https://mevofvd5omld.c01-14.plentymarkets.com >> apps/server/.env - name: Install Dependencies - run: yarn --frozen-lockfile + run: yarn --immutable - - name: Build application - run: yarn build + - name: Unit tests + run: | + touch apps/server/.env + echo API_ENDPOINT=https://mevofvd5omld.c01-14.plentymarkets.com >> apps/server/.env + pushd ${{ github.workspace }}/apps/server/ + yarn start & + pushd ${{ github.workspace }}/apps/web/ + yarn test:coverage i18n.spec.ts + pushd -0 && dirs -c - - uses: cypress-io/github-action@v5 + - name: E2E tests + uses: cypress-io/github-action@v5 with: + build: yarn build command: yarn test:cypress + install: false + env: + PAYPAL_EMAIL: ${{ secrets.PAYPAL_EMAIL }} + PAYPAL_PASSWORD: ${{ secrets.PAYPAL_PASSWORD }} + - uses: actions/upload-artifact@v3 if: failure() with: diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml deleted file mode 100644 index 88741fb99..000000000 --- a/.github/workflows/unit-tests.yaml +++ /dev/null @@ -1,60 +0,0 @@ -name: Unit Tests - -on: - pull_request: - branches: - - 'main' - -permissions: - contents: read - packages: read - -jobs: - unit-tests: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: '20.5' - cache: 'yarn' - registry-url: "https://npm.pkg.github.com" - - - name: Set yarn version - run: | - yarn set version stable - yarn set version 3.6.4 - - - name: Setup .yarnrc.yml - run: | - yarn config set nodeLinker node-modules - yarn config set npmScopes.plentymarkets.npmRegistryServer "https://npm.pkg.github.com" - yarn config set npmScopes.plentymarkets.npmAlwaysAuth true - yarn config set npmScopes.plentymarkets.npmAuthToken $NODE_AUTH_TOKEN - env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Install Dependencies - run: yarn --immutable - - - name: Start server - run: | - pushd apps/server - yarn start & - popd - - - name: Run vitest - run: | - pushd apps/web - yarn test:coverage i18n.spec.ts - -# - name: Report Coverage -# if: always() -# uses: davelosert/vitest-coverage-report-action@v2 -# with: -# file-coverage-mode: all -# name: Unit Test Coverage diff --git a/.github/workflows/upload.yml b/.github/workflows/upload.yml index 69328ccb6..6cefaac57 100644 --- a/.github/workflows/upload.yml +++ b/.github/workflows/upload.yml @@ -24,6 +24,9 @@ jobs: with: ref: ${{ inputs.tag-version }} + - name: Enable corepack + run: corepack enable + - name: Setup node uses: actions/setup-node@v3 with: diff --git a/.prettierignore b/.prettierignore index af72bce21..1e30caed1 100755 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ node_modules +__tests__ .nuxt .turbo diff --git a/.prettierrc b/.prettierrc index 15cfc7d6a..6c04e63d5 100755 --- a/.prettierrc +++ b/.prettierrc @@ -6,11 +6,5 @@ "tabWidth": 2, "trailingComma": "all", "printWidth": 120, - "jsxSingleQuote": false, - "importOrder": [ - "^vue", - "^(nuxt/(.*)$)|^(nuxt$)", - "", - "^[./]" - ] + "jsxSingleQuote": false } diff --git a/GUIDE.md b/GUIDE.md index 13d6b7502..b11bad96f 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -139,7 +139,7 @@ Naming convention: - each composables should be prefixed with `use` keyword (`useProduct`) - composable should follow `Camel case` pattern (`useProductReviews`) -We are using internal [Nuxt.js state management](https://nuxt.com/docs/getting-started/state-management) to lock the data responsibility along with the composition functions (composables). State connected to the certain composable is read-only and modified by the internal modifiers (functions). You can check the `useCart.ts` composable as a reference. +We are using internal [Nuxt.js state management](https://nuxt.com/docs/getting-started/state-management) to lock the data responsibility along with the composition functions (composables). State connected to the certain composable is read-only and modified by the internal modifiers (functions). You can check the `useCart.ts` composable as a reference. ### Components @@ -191,7 +191,7 @@ Naming convention: The data fetching process is handled seamlessly by integrating VSF SDK, which acts as a robust communication layer between the application and the VSF Middleware. The SDK provides a set of convenient and optimized methods to fetch data from various APIs and services. -To simplify the implementation and management of data fetching we're using native Nuxt's composables for [useAsyncData](https://nuxt.com/docs/api/composables/use-async-data) and [useState](https://nuxt.com/docs/getting-started/state-management). It seamlessly integrates with VSF SDK and simplifies the process of caching, synchronizing, and managing the application's data. These functions provide an elegant and efficient way to handle asynchronous data fetching, automatically managing data caching, refetching, and background updates. +To simplify the implementation and management of data fetching we're using native Nuxt's composables for [useAsyncData](https://nuxt.com/docs/api/composables/use-async-data) and [useState](https://nuxt.com/docs/getting-started/state-management). It seamlessly integrates with VSF SDK and simplifies the process of caching, synchronizing, and managing the application's data. These functions provide an elegant and efficient way to handle asynchronous data fetching, automatically managing data caching, refetching, and background updates. By combining VSF SDK with Nuxt's composables, this project ensures a reliable and performant data fetching experience for the application. Developers can easily fetch, update, and maintain data using composition declarative approach, while the VSF SDK handles the underlying communication and data retrieval tasks. @@ -244,7 +244,7 @@ In order to optimize and enhance the performance of the application, several per > **Note** > To analyze your app bundles run `npx nuxi analyze` command. -*You can use bundle analyzer to check the whole application structure along with server, or simply jump into the `apps/web` folder to analyze your web application.* +_You can use bundle analyzer to check the whole application structure along with server, or simply jump into the `apps/web` folder to analyze your web application._ #### Web performance automated testing diff --git a/LICENSE.md b/LICENSE.md index ea0ce1003..a713e6590 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -18,4 +18,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE -OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index f72c0093e..44339ec20 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
Vue Storefront - plentysystems logo +plentysystems logo

Vue Storefront 3 Integration With plentysystems

@@ -25,20 +25,20 @@ For plentyShop PWA to run, you have to install plugins that provide additional R 1. Log into your plentysystems system. 2. [Install](https://knowledge.plentymarkets.com/en-gb/manual/main/plugins/installing-added-plugins.html#installing-plugins) the following plugins in the latest version: - - IO - - plentyShop LTS - - PayPal - - Cash in advance - - Pay upon pickup - - DHL Shipping (Versenden) - - Customer feedback + - IO + - plentyShop LTS + - PayPal + - Cash in advance + - Pay upon pickup + - DHL Shipping (Versenden) + - Customer feedback 3. Set the priorities of the plugins, so that IO has the highest priority and plentyShop LTS the second highest. The priorities of all other plugins remains 0. ### System configuration 1. Log into your plentysystems system. 2. Go to **Setup » Guided Tours** and complete **Setting up plentyShop**. -3. *Optional:* Go to **Setup » Orders » Payment » PayPal** and set up your PayPal account. +3. _Optional:_ Go to **Setup » Orders » Payment » PayPal** and set up your PayPal account. ## Local dev setup @@ -94,6 +94,7 @@ For detailed setup instructions, refer to the [deployment guide](./docs/deployme ## Resources ### Changelog + [English](./docs/changelog/changelog_en.md) | [German](./docs/changelog/changelog_de.md) ### Features diff --git a/apps/server/middleware.config.ts b/apps/server/middleware.config.ts index 30b93c1e6..5fcf1927a 100644 --- a/apps/server/middleware.config.ts +++ b/apps/server/middleware.config.ts @@ -7,10 +7,10 @@ const config = { location: '@plentymarkets/shop-api/server', configuration: { api: { - url: process.env.API_ENDPOINT - } + url: process.env.API_ENDPOINT, + }, }, - } + }, }, }; diff --git a/apps/server/package.json b/apps/server/package.json index af0a4fa07..56fffccef 100755 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "server", - "version": "0.1.0", + "version": "1.2.0", "main": "./src/index.ts", "license": "MIT", "scripts": { diff --git a/apps/server/tsup.config.ts b/apps/server/tsup.config.ts index 9c5f7d4d5..7e920546f 100644 --- a/apps/server/tsup.config.ts +++ b/apps/server/tsup.config.ts @@ -1,8 +1,8 @@ -import { defineConfig } from 'tsup' +import { defineConfig } from 'tsup'; export default defineConfig({ entry: ['src/index.ts'], splitting: false, sourcemap: true, - clean: true -}) + clean: true, +}); diff --git a/apps/web/.eslintrc.cjs b/apps/web/.eslintrc.cjs index beb0288a9..712d06979 100644 --- a/apps/web/.eslintrc.cjs +++ b/apps/web/.eslintrc.cjs @@ -32,5 +32,7 @@ module.exports = { 'unicorn/filename-case': 'off', 'no-undef': 'off', 'vue/no-setup-props-destructure': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/max-attributes-per-line': 'off', }, }; diff --git a/apps/web/__tests__/support/commands.ts b/apps/web/__tests__/support/commands.ts index 9ca4e728f..bdfa76d46 100644 --- a/apps/web/__tests__/support/commands.ts +++ b/apps/web/__tests__/support/commands.ts @@ -55,4 +55,4 @@ Cypress.Commands.add('visitAndHydrate', (url, options) => { cy.visit(url, options); // Wait until app is hydrated cy.get('body.hydrated'); -}); +}); \ No newline at end of file diff --git a/apps/web/__tests__/support/pageObjects/CategoryObject.ts b/apps/web/__tests__/support/pageObjects/CategoryObject.ts index f59c3a6a8..8ea710eba 100644 --- a/apps/web/__tests__/support/pageObjects/CategoryObject.ts +++ b/apps/web/__tests__/support/pageObjects/CategoryObject.ts @@ -1,7 +1,7 @@ export class CategoryPageObject { get filterClickShouldReloadCategory() { - cy.intercept('http://localhost:8181/plentysystems/getFacet').as('getFacet'); + cy.intercept('/plentysystems/getFacet').as('getFacet'); cy.getByTestId('category-filter-0').first().click(); cy.wait('@getFacet').its('response.statusCode').should('eq', 200) return this; diff --git a/apps/web/__tests__/support/pageObjects/LanguageSelectObject.ts b/apps/web/__tests__/support/pageObjects/LanguageSelectObject.ts new file mode 100644 index 000000000..dd3d0f1da --- /dev/null +++ b/apps/web/__tests__/support/pageObjects/LanguageSelectObject.ts @@ -0,0 +1,29 @@ +export class LanguageSelectObject { + checkIfModalIsOpen() { + cy.getByTestId('languageSelectList').first().should('be.visible'); + return this; + } + + checkOptions() { + cy.getByTestId('languageOption-en').should('be.visible'); + cy.getByTestId('languageOption-de').should('be.visible'); + // for consistency should also check for flags svg present + return this; + } + + openModal() { + cy.getByTestId('open-languageselect-button').first().click();; + return this; + } + + checkLanguageSelected(locale: string) { + cy.getCookie('vsf-locale').should('have.property', 'value', locale); + + return this; + } + + selectOption(option: string) { + cy.getByTestId(`languageOption-${option}`).first().click();; + return this; + } +} diff --git a/apps/web/__tests__/support/pageObjects/ProductListPageObject.ts b/apps/web/__tests__/support/pageObjects/ProductListPageObject.ts index 6fba34d70..a7c22304e 100644 --- a/apps/web/__tests__/support/pageObjects/ProductListPageObject.ts +++ b/apps/web/__tests__/support/pageObjects/ProductListPageObject.ts @@ -70,7 +70,7 @@ export class ProductListPageObject { } addToCart() { - this.products.first().find(`[data-testid="button"]`).click(); + this.products.find(`[data-testid="add-to-basket-short"]`).first().click(); return this; } diff --git a/apps/web/__tests__/test/smoke/cartPage.cy.ts b/apps/web/__tests__/test/smoke/cartPage.cy.ts index dd982edc8..698239c93 100644 --- a/apps/web/__tests__/test/smoke/cartPage.cy.ts +++ b/apps/web/__tests__/test/smoke/cartPage.cy.ts @@ -1,19 +1,24 @@ import { CartPageObject } from '../../support/pageObjects/CartPageObject'; -import { HomePageObject } from '../../support/pageObjects/HomePageObject'; import { ProductListPageObject } from '../../support/pageObjects/ProductListPageObject'; -import { paths } from '../../../utils/paths'; const cart = new CartPageObject(); -const homePage = new HomePageObject(); const productListPage = new ProductListPageObject(); describe('Smoke: Cart Page', () => { - it('[smoke] Add items to cart and display it', () => { - cy.visit(paths.home); - homePage.goToCategory(); - productListPage.addToCart() + beforeEach(() => { + cy.setCookie('vsf-locale', 'en'); + cy.setCookie('consent-cookie', '{"Essentials":{"Session":true,"Consent":true,"Session2":true},"External Media":{"Session":false,"Consent":false,"Session2":false},"Functional":{"Session":false,"Consent":false,"Session2":false},"Marketing":{"Session":false,"Consent":false,"Session2":false}}') + }); + + + it('[smoke] Add items to cart and display it', () => { + cy.visitAndHydrate('/c/living-room'); + cy.intercept('/plentysystems/doAddCartItem').as('doAddCartItem'); + productListPage.addToCart(); + cy.wait('@doAddCartItem'); + cart.openCart().checkCart(); }); }); diff --git a/apps/web/__tests__/test/smoke/categoryPage.cy.ts b/apps/web/__tests__/test/smoke/categoryPage.cy.ts index 0795cec7e..6aacdae3d 100644 --- a/apps/web/__tests__/test/smoke/categoryPage.cy.ts +++ b/apps/web/__tests__/test/smoke/categoryPage.cy.ts @@ -3,6 +3,11 @@ import { CategoryPageObject } from '../../support/pageObjects/CategoryObject'; const category = new CategoryPageObject(); describe('Smoke: Category Page', () => { + beforeEach(() => { + cy.setCookie('vsf-locale', 'en'); + cy.setCookie('consent-cookie', '{"Essentials":{"Session":true,"Consent":true,"Session2":true},"External Media":{"Session":false,"Consent":false,"Session2":false},"Functional":{"Session":false,"Consent":false,"Session2":false},"Marketing":{"Session":false,"Consent":false,"Session2":false}}') + }); + it('[smoke] Category filters should trigger a product data reload', () => { // We should configure the system so that the first category is set up with filters. // This way we are independet from the language and the url. diff --git a/apps/web/__tests__/test/smoke/languageSelect.cy.ts b/apps/web/__tests__/test/smoke/languageSelect.cy.ts new file mode 100644 index 000000000..634f8f9be --- /dev/null +++ b/apps/web/__tests__/test/smoke/languageSelect.cy.ts @@ -0,0 +1,20 @@ +import { LanguageSelectObject } from '../../support/pageObjects/LanguageSelectObject'; +import { paths } from '../../../utils/paths'; + +const languageSelect = new LanguageSelectObject(); + +beforeEach(() => { + cy.visitAndHydrate(paths.home); + cy.clearCookies(); +}); + +describe('Smoke: Language Selector', () => { + + it('[smoke] Check if Language selector works', () => { + languageSelect.openModal(); + languageSelect.checkIfModalIsOpen(); + languageSelect.checkOptions(); + languageSelect.selectOption('de'); + languageSelect.checkLanguageSelected('de'); + }); +}); diff --git a/apps/web/app.vue b/apps/web/app.vue index 7237a5df7..1dfb8365b 100644 --- a/apps/web/app.vue +++ b/apps/web/app.vue @@ -7,8 +7,15 @@ diff --git a/apps/web/components/ui/ProductCard/ProductCard.vue b/apps/web/components/ui/ProductCard/ProductCard.vue index 6b4ecad4c..4bd3ded94 100644 --- a/apps/web/components/ui/ProductCard/ProductCard.vue +++ b/apps/web/components/ui/ProductCard/ProductCard.vue @@ -38,7 +38,7 @@
- {{ $t('account.ordersAndReturns.orderDetails.priceFrom') }} {{ $n(cheapestPrice ?? mainPrice, 'currency') }} @@ -55,6 +55,7 @@ v-if="productGetters.canBeAddedToCartFromCategoryPage(product)" size="sm" class="min-w-[80px] w-fit" + data-testid="add-to-basket-short" @click="addWithLoader(Number(productGetters.getId(product)))" :disabled="loading" > @@ -119,18 +120,10 @@ const mainPrice = computed(() => { return 0; }); -const cheapestPrice = computed(() => { - const price = productGetters.getCheapestGraduatedPrice(product); - if (price && price < mainPrice.value) { - return price; - } - return null; -}); -const oldPrice = productGetters.getRegularPrice(product); +const cheapestPrice = productGetters.getCheapestGraduatedPrice(product); +const oldPrice = productGetters.getRegularPrice(product); const path = computed(() => productGetters.getCategoryUrlPath(product, categoryTree.value)); - const productSlug = computed(() => productGetters.getSlug(product) + `_${productGetters.getItemId(product)}`); - const NuxtLink = resolveComponent('NuxtLink'); diff --git a/apps/web/composables/useActiveShippingCountries/useActiveShippingCountries.ts b/apps/web/composables/useActiveShippingCountries/useActiveShippingCountries.ts index 7e6b5c2c8..4da7f63be 100644 --- a/apps/web/composables/useActiveShippingCountries/useActiveShippingCountries.ts +++ b/apps/web/composables/useActiveShippingCountries/useActiveShippingCountries.ts @@ -4,10 +4,15 @@ import { useSdk } from '~/sdk'; import { UseActiveShippingCountriesReturn, UseActiveShippingCountriesState, GetActiveShippingCountries } from './types'; /** - * @description Composable for getting all active shipping countries. + * @description Composable for getting an array of `ActiveShippingCountry`. * @example * ``` ts - * const { data, loading, getActiveShippingCountries } = useActiveShippingCountries(); + * const { + * data, + * loading, + * getActiveShippingCountries + * } = useActiveShippingCountries(); + * getActiveShippingCountries(); * ``` */ @@ -17,13 +22,6 @@ export const useActiveShippingCountries: UseActiveShippingCountriesReturn = () = loading: false, })); - /** - * @description Function to get all active shipping countries. - * @example - * ``` ts - * getActiveShippingCountries(); - * ``` - */ const getActiveShippingCountries: GetActiveShippingCountries = async () => { state.value.loading = true; const { data, error } = await useAsyncData('getActiveShippingCountries', () => diff --git a/apps/web/composables/useAddress/useAddress.ts b/apps/web/composables/useAddress/useAddress.ts index 40d69a17f..189ebb8d4 100644 --- a/apps/web/composables/useAddress/useAddress.ts +++ b/apps/web/composables/useAddress/useAddress.ts @@ -6,18 +6,38 @@ import { useSdk } from '~/sdk'; import { UseAddressReturn, GetAddresses, SaveAddress, UseAddressMethodsState } from './types'; /** - * @description Composable for getting addresses from the current user session. - * @param type {@link AddressType} + * @description Composable for working with addresses in the current user session. + * The composable covers two types of addresses, billing and shipping. + * @param {@link AddressType} * @example + * This example uses the address type `Billing`. All examples are equivalent for addresses of type `Shipping`. * ``` ts * const { - * data, loading, getAddresses, defaultAddressId, saveAddress, deleteAddress, setDefault + * data, + * loading, + * defaultAddressId, + * savedAddress, + * getAddresses, + * saveAddress, + * deleteAddress, + * setDefault * } = useAddress(AddressType.Billing); - * - * const { - * data, loading, getAddresses, defaultAddressId, saveAddress, deleteAddress, setDefault - * } = useAddress(AddressType.Shipping); + * let address: Address; + * let id: Number; + * getAddresses(); + * saveAddress(address); + * deleteAddress(id); + * setDefault(id); * ``` + * - `getAddresses` gets all addresses of the address type passed to `useAddress`. + * Updates `defaultAddressId` to the current default address. + * - `saveAddress` saves the given address with the address type passed to `useAddress`. + * If successful, it returns the `savedAddress`. + * After saving the address, updates the list of addresses. + * - `deleteAddress` deletes the address of the address type passed to `useAddress` with the given ID. + * After deleting the address, updates the list of addresses. + * - `setDefault` updates the `defaultAddressId` of the type passed to `useAddress` with the given ID. + * After setting the default, updates the list of addresses. */ export const useAddress: UseAddressReturn = (type: AddressType) => { @@ -28,13 +48,6 @@ export const useAddress: UseAddressReturn = (type: AddressType) => { defaultAddressId: 0, })); - /** - * @description Get the default address. - * @example - * ``` ts - * getDefaultAddress() - * ``` - */ const getDefaultAddress = (): void => { state.value.loading = true; @@ -51,13 +64,6 @@ export const useAddress: UseAddressReturn = (type: AddressType) => { state.value.loading = false; }; - /** - * @description Function for fetching addresses based on type. - * @example - * ``` ts - * getAddresses(); - * ``` - */ const getAddresses: GetAddresses = async () => { state.value.loading = true; const { data, error } = await useAsyncData(type.toString(), () => @@ -74,14 +80,6 @@ export const useAddress: UseAddressReturn = (type: AddressType) => { return state.value.data; }; - /** - * @description Save an address. - * @param address { Address } - * @example - * ``` ts - * saveAddress(address); - * ``` - */ const saveAddress: SaveAddress = async (address: Address) => { state.value.loading = true; const { data, error } = await useAsyncData(type.toString(), () => @@ -98,14 +96,6 @@ export const useAddress: UseAddressReturn = (type: AddressType) => { return state.value.savedAddress; }; - /** - * @description Set the default address. - * @param addressId - * @example - * ``` ts - * setDefault(1); - * ``` - */ const setDefault: SetDefault = async (addressId: number) => { state.value.loading = true; await useSdk().plentysystems.setAddressAsDefault({ @@ -118,14 +108,6 @@ export const useAddress: UseAddressReturn = (type: AddressType) => { await getAddresses(); }; - /** - * @description Delete an address. - * @param addressId - * @example - * ``` ts - * deleteAddress(1); - * ``` - */ const deleteAddress: DeleteAddress = async (addressId: number) => { state.value.loading = true; await useSdk().plentysystems.deleteAddress({ diff --git a/apps/web/composables/useCategoryTree/types.ts b/apps/web/composables/useCategoryTree/types.ts index 3007dd675..49c281dd2 100644 --- a/apps/web/composables/useCategoryTree/types.ts +++ b/apps/web/composables/useCategoryTree/types.ts @@ -7,11 +7,13 @@ export interface UseCategoryTreeState { } export type GetCategoryTree = () => Promise; +export type SetCategoryTree = (data: CategoryTreeItem[]) => void; export interface UseCategoryTreeMethods { data: Readonly>; loading: Readonly>; getCategoryTree: GetCategoryTree; + setCategoryTree: SetCategoryTree; } export type UseCategoryTreeMethodsReturn = () => UseCategoryTreeMethods; diff --git a/apps/web/composables/useCategoryTree/useCategoryTree.ts b/apps/web/composables/useCategoryTree/useCategoryTree.ts index bc8c0c641..860707df2 100644 --- a/apps/web/composables/useCategoryTree/useCategoryTree.ts +++ b/apps/web/composables/useCategoryTree/useCategoryTree.ts @@ -1,7 +1,7 @@ import type { CategoryTreeItem } from '@plentymarkets/shop-api'; import { toRefs } from '@vueuse/shared'; import { useSdk } from '~/sdk'; -import type { UseCategoryTreeState, UseCategoryTreeMethodsReturn, GetCategoryTree } from './types'; +import type { UseCategoryTreeState, UseCategoryTreeMethodsReturn, GetCategoryTree, SetCategoryTree } from './types'; /** * @description Composable for managing the category tree. @@ -38,8 +38,20 @@ export const useCategoryTree: UseCategoryTreeMethodsReturn = () => { } }; + /** + * @description Function for setting the category tree data. + * @example + * ``` ts + * setCategoryTree(); + * ``` + */ + const setCategoryTree: SetCategoryTree = (data: CategoryTreeItem[]) => { + state.value.data = data; + }; + return { getCategoryTree, + setCategoryTree, ...toRefs(state.value), }; }; diff --git a/apps/web/composables/useCsrfToken/index.ts b/apps/web/composables/useCsrfToken/index.ts new file mode 100644 index 000000000..249974836 --- /dev/null +++ b/apps/web/composables/useCsrfToken/index.ts @@ -0,0 +1 @@ +export * from './useCsrfToken'; diff --git a/apps/web/composables/useCsrfToken/useCsrfToken.ts b/apps/web/composables/useCsrfToken/useCsrfToken.ts new file mode 100644 index 000000000..32bc756b0 --- /dev/null +++ b/apps/web/composables/useCsrfToken/useCsrfToken.ts @@ -0,0 +1,9 @@ +export const useCsrfToken = () => { + const state = useState(`useCsrfToken`, () => ({ + token: '', + })); + + return { + ...toRefs(state.value), + }; +}; diff --git a/apps/web/composables/useInitalSetup/types.ts b/apps/web/composables/useInitalSetup/types.ts index 6dfc18637..7b2c8b8a1 100644 --- a/apps/web/composables/useInitalSetup/types.ts +++ b/apps/web/composables/useInitalSetup/types.ts @@ -6,6 +6,7 @@ export interface UseInitialSetupState { export interface UseInitialSetup { setInitialData: SetInitialData; + setInitialDataSSR: SetInitialData; ssrLocale: Ref; } diff --git a/apps/web/composables/useInitalSetup/useInitialSetup.ts b/apps/web/composables/useInitalSetup/useInitialSetup.ts index 1ba18c065..3804bbd08 100644 --- a/apps/web/composables/useInitalSetup/useInitialSetup.ts +++ b/apps/web/composables/useInitalSetup/useInitialSetup.ts @@ -14,11 +14,42 @@ const setInitialData: SetInitialData = async () => { const { setCart, loading: cartLoading } = useCart(); cartLoading.value = true; + const { data, error } = await useAsyncData(() => useSdk().plentysystems.getSession()); useHandleError(error.value); - setUser(data.value?.data as SessionResult); - setCart(data.value?.data.basket as Cart); + if (data.value?.data) { + setUser(data.value?.data as SessionResult); + setCart(data.value?.data.basket as Cart); + } + + cartLoading.value = false; + + return true; +}; + +/** Function for getting category tree and current customer/cart data from session + * @return SetInitialData + * @example + * ``` ts + * setInitialDataSSR(); + * ``` + */ +const setInitialDataSSR: SetInitialData = async () => { + const { setUser } = useCustomer(); + const { setCategoryTree } = useCategoryTree(); + const { setCart, loading: cartLoading } = useCart(); + + cartLoading.value = true; + + const { data, error } = await useAsyncData(() => useSdk().plentysystems.getInit()); + useHandleError(error.value); + + if (data.value?.data) { + setUser(data.value?.data.session as SessionResult); + setCart(data.value?.data.session.basket as Cart); + setCategoryTree(data.value.data.categories); + } cartLoading.value = false; @@ -40,6 +71,7 @@ export const useInitialSetup: UseInitialSetupReturn = () => { return { setInitialData, + setInitialDataSSR, ...toRefs(state.value), }; }; diff --git a/apps/web/composables/useMakeOrder/useMakeOrder.ts b/apps/web/composables/useMakeOrder/useMakeOrder.ts index 04500c6e3..b1a28fbf3 100644 --- a/apps/web/composables/useMakeOrder/useMakeOrder.ts +++ b/apps/web/composables/useMakeOrder/useMakeOrder.ts @@ -30,6 +30,7 @@ export const useMakeOrder: UseMakeOrderReturn = () => { * ``` */ const createOrder: CreateOrder = async (params: MakeOrderParams) => { + const { $i18n } = useNuxtApp(); state.value.loading = true; await useAsyncData(() => @@ -44,47 +45,57 @@ export const useMakeOrder: UseMakeOrderReturn = () => { const { data: preparePaymentData, error: preparePaymentError } = await useAsyncData(() => useSdk().plentysystems.doPreparePayment(), ); + useHandleError(preparePaymentError.value); const paymentType = preparePaymentData.value?.data.type || 'errorCode'; const paymentValue = preparePaymentData.value?.data.value || '""'; + const continueOrHtmlContent = async () => { + const { data, error } = await useAsyncData(() => useSdk().plentysystems.doPlaceOrder()); + + useHandleError(error.value); + + if (error.value) { + state.value.loading = false; + return {} as Order; + } + + state.value.data = data.value?.data ?? state.value.data; + + await useAsyncData(() => + useSdk().plentysystems.doExecutePayment({ + orderId: state.value.data.order.id, + paymentId: params.paymentId, + }), + ); + }; + switch (paymentType) { case 'continue': case 'htmlContent': { - const { data, error } = await useAsyncData(() => useSdk().plentysystems.doPlaceOrder()); - useHandleError(error.value); - if (error.value) { - state.value.loading = false; - return {} as Order; - } - - state.value.data = data.value?.data ?? state.value.data; - - await useAsyncData(() => - useSdk().plentysystems.doExecutePayment({ - orderId: state.value.data.order.id, - paymentId: params.paymentId, - }), - ); + await continueOrHtmlContent(); break; } + case 'redirectUrl': { // redirect to given payment provider window.location.assign(paymentValue); break; } + case 'externalContentUrl': { // show external content in iframe break; } case 'errorCode': { - // NotificationService.error(paymentValue); + useHandleError({ message: paymentValue }); break; } + default: { - // NotificationService.error("Unknown response from payment provider: " + paymentType); + useHandleError({ message: $i18n.t('orderErrorProvider', { paymentType: paymentType }) }); break; } } diff --git a/apps/web/docs/.vitepress/config.ts b/apps/web/docs/.vitepress/config.ts index aef403c81..213558007 100644 --- a/apps/web/docs/.vitepress/config.ts +++ b/apps/web/docs/.vitepress/config.ts @@ -1,15 +1,15 @@ -import { defineConfig } from 'vitepress' -import typedocSidebar from '../composables/typedoc-sidebar.json'; +import { defineConfig } from 'vitepress'; +import typedocSidebar from '../reference/composables/typedoc-sidebar.json'; // https://vitepress.dev/reference/site-config export default defineConfig({ - title: "plentyShop PWA Docs", - description: "Documentation of plentyShop PWA", + title: 'plentyShop PWA Docs', + description: 'Documentation of plentyShop PWA', themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Home', link: '/' }, - { text: 'Composables', link: '/composables/' } + { text: 'Composables', link: '/reference/composables/' }, ], sidebar: [ @@ -19,8 +19,6 @@ export default defineConfig({ }, ], - socialLinks: [ - { icon: 'github', link: 'https://github.com/vuejs/vitepress' } - ] - } -}) + socialLinks: [{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }], + }, +}); diff --git a/apps/web/docs/index.md b/apps/web/docs/index.md index 8465aa275..c26f46422 100644 --- a/apps/web/docs/index.md +++ b/apps/web/docs/index.md @@ -3,7 +3,7 @@ layout: home hero: - name: "plentyShop PWA" + name: 'plentyShop PWA' tagline: Composables documentation actions: - theme: brand @@ -21,4 +21,3 @@ features: - title: Functions details: Lorem ipsum dolor sit amet, consectetur adipiscing elit --- - diff --git a/apps/web/lang/de.json b/apps/web/lang/de.json index 699df82c7..0ef008110 100644 --- a/apps/web/lang/de.json +++ b/apps/web/lang/de.json @@ -31,6 +31,7 @@ "asterixHint": "* markierte Zustimmungen sind erforderlich" }, "CookieBar": { + "Cookie Settings": "Cookie-Einstellungen", "Privacy Settings": "Datenschutzeinstellungen", "More information": "Mehr Informationen", "Further Settings": "Weitere Einstellungen", @@ -175,7 +176,7 @@ "quantitySelectorIncrease": "Menge erhöhen", "scrollTop": "Nach oben scrollen", "numberInCart": "{count} Produkte im Warenkorb", - "languageSelector": "Sprachauswahl", + "languageSelector": "Sprachauswahl öffnen", "allProductsLinkText": "Produkte durchsuchen", "breadcrumbsDropdownText": "Mehr Breadcrumbs anzeigen", "youAreOfflineText": "Es scheint, dass Sie derzeit offline sind, überprüfen Sie Ihre Internetverbindung", @@ -279,6 +280,7 @@ "backToCheckout": "Zurück zur Kasse", "continueShopping": "Einkauf fortsetzen", "orderErrorMessage": "Etwas ging schief. Versuchen Sie es später noch einmal.", + "orderErrorProvider": "Unbekannte Antwort des Zahlungsanbieters: {paymentType}", "orderNumber": "Auftrags-ID", "productDetails": "Produkt-Details", "technicalData": "Technische Daten", diff --git a/apps/web/lang/en.json b/apps/web/lang/en.json index 767297c2f..1feee6651 100644 --- a/apps/web/lang/en.json +++ b/apps/web/lang/en.json @@ -31,6 +31,7 @@ "asterixHint": "* marked consents are required" }, "CookieBar": { + "Cookie Settings": "Cookie Settings", "Privacy Settings": "Privacy Settings", "More information": "More information", "Further Settings": "Further Settings", @@ -163,7 +164,7 @@ "addedToCart": "Item added to Cart", "deletedFromCart": "Item removed from shopping cart", "search": "Search", - "openSearchModalButtonLabel": "Click to open search modal", + "openSearchModalButtonLabel": "Open item search", "vsfHomepage": "VSF Homepage", "home": "Home", "products": "Products", @@ -175,7 +176,7 @@ "quantitySelectorIncrease": "Increase value", "scrollTop": "Scroll to top", "numberInCart": "{count} product in cart", - "languageSelector": "Language selection", + "languageSelector": "Open language select", "allProductsLinkText": "Browse products", "breadcrumbsDropdownText": "Show more breadcrumbs", "youAreOfflineText": "It seems you are currently offline, check your internet connection", @@ -279,6 +280,7 @@ "backToCheckout": "Back to Checkout", "continueShopping": "Continue shopping", "orderErrorMessage": "Something went wrong. Try again later.", + "orderErrorProvider": "Unknown response from payment provider: {paymentType}", "orderNumber": "Order ID", "productDetails": "Product details", "technicalData": "Technical data", diff --git a/apps/web/layouts/default.vue b/apps/web/layouts/default.vue index b24105aef..1e29c5083 100644 --- a/apps/web/layouts/default.vue +++ b/apps/web/layouts/default.vue @@ -1,169 +1,177 @@ diff --git a/apps/web/pages/checkout.vue b/apps/web/pages/checkout.vue index f4391e6c5..ebd82f931 100644 --- a/apps/web/pages/checkout.vue +++ b/apps/web/pages/checkout.vue @@ -227,25 +227,26 @@ const openPayPalCardDialog = () => { paypalCardDialog.value = true; }; -const order = async () => { - if (!validateAddresses() || !validateTerms()) { - return; +const handleRegularOrder = async () => { + const data = await createOrder({ + paymentId: paymentMethodData.value.selected, + shippingPrivacyHintAccepted: shippingPrivacyAgreement.value, + }); + + clearCartItems(); + + if (data?.order?.id) { + navigateTo(localePath(paths.thankYou + '/?orderId=' + data.order.id + '&accessKey=' + data.order.accessKey)); } - const paymentMethodsById = _.keyBy(paymentMethods.value.list, 'id'); +}; - if (paymentMethodsById[selectedPaymentId.value].key === 'plentyPayPal') { - paypalCardDialog.value = true; - } else { - const data = await createOrder({ - paymentId: paymentMethodData.value.selected, - shippingPrivacyHintAccepted: shippingPrivacyAgreement.value, - }); +const order = async () => { + if (!validateAddresses() || !validateTerms()) return; - clearCartItems(); + const paymentMethodsById = _.keyBy(paymentMethods.value.list, 'id'); - if (data?.order?.id) { - navigateTo(localePath(paths.thankYou + '/?orderId=' + data.order.id + '&accessKey=' + data.order.accessKey)); - } - } + paymentMethodsById[selectedPaymentId.value].key === 'plentyPayPal' + ? (paypalCardDialog.value = true) + : await handleRegularOrder(); }; diff --git a/apps/web/pages/index.vue b/apps/web/pages/index.vue index 32f105be3..71b05ad6a 100644 --- a/apps/web/pages/index.vue +++ b/apps/web/pages/index.vue @@ -1,22 +1,16 @@