diff --git a/storefront/BEST_PRACTICES.md b/storefront/BEST_PRACTICES.md deleted file mode 100644 index 391a1a40a9..0000000000 --- a/storefront/BEST_PRACTICES.md +++ /dev/null @@ -1,106 +0,0 @@ -## Best practices for Storefront - -### Destructuring props - -- code clarity, easy way to set the default value - -```ts -const RangeSlider: FC = ({ - min, - max, - delay = 300, - minValue, - maxValue, - setMinValue, - setMaxValue, - dispatchMinValue, - dispatchMaxValue, -}) => { - ... -} -``` - -### Static constants above the component (using SCREAMING_CASE) - -- code clarity, it is not initialized every time a component is rendered - -```ts -const TEST_IDENTIFIER = 'blocks-product-filter'; - -const Filter: FC = ({ productFilterOptions, slug, formUpdateDependency }) => { - ... -} -``` - -### Handlers with parameters as double arrow functions - -- code clarity, useCallback can be used to memoize the function - -wrong way: - -```tsx -// no useCallback -const mySuperHandler = (id: number) => { - // do something -}; - -// with useCallback -const mySuperHandler = useCallback((id: number) => { - // do something -}, []); - - mySuperHandler(1)}>Click me -``` - -good way: - -```tsx -// no useCallback -const mySuperHandler = (id: number) => () => { - // do something -}; - -// with useCallback -const mySuperHandler = useCallback((id: number) => () => { - // do something -}, []); - -Click me -``` - -### \_\_typename in the GraphQL fragments - -- we use the `__typename` for business logic a lot in our codebase -- there is a bug (or a behavior) in the URQl package that causes the `__typename` to be missing when it is read from the cache -- to ensure that the `__typename` is always available, we add it to the fragments -- - -### Don't use default exports and index files - -- improves DX thanks to better components' usage searchability - -```tsx -export const MySuperComponent = () => { - ... -} -``` - -### Don't spread props everywhere - -- spread only the props that are needed or destructure all props and use only the ones that are needed - -wrong way: - -```tsx - - - -``` - -good way: - -```tsx - - - -``` diff --git a/storefront/README.md b/storefront/README.md index 2be3a1e4de..6a4cd02b7a 100644 --- a/storefront/README.md +++ b/storefront/README.md @@ -1,68 +1,3 @@ -This is documentation for Shopsys Platform StoreFront. Let's start with first two steps. +# Shopsys Platform StoreFront -## Install - -1 - Install all dependencies. - -```plain -pnpm install -``` - -## Start app - -2 - Start the development server. - -```plain -pnpm run dev -``` - -After this command open http://localhost:3000/ in your browser. - -### Optional - -Build the app for production. - -```plain -pnpm run build -``` - -Run the built app in production mode. - -```plain -pnpm start -``` - -Run eslint for code - -```plain -pnpm run lint -``` - -Run eslint and fix code - -```plain -pnpm run lint--fix -``` - -Run prettier format code - -```plain -pnpm run format -``` - -Run translation files generator. You can find generated files in /public/locales/ folder. - -```plain -pnpm run translate -``` - -## Development - -### Generated GraphQL requests - -Requests to API are generated by [GraphQl Code Generator](https://www.graphql-code-generator.com/). The definitions of queries/mutations/fragments are stored in `./graphql/requests`. - -After some changes to API you need to regenerate hooks and types, so it's in sync with current state of Frontend API: This is automatically checked on the CI pipeline. If the hooks and types are not regenerated, the pipeline fails. - -1. Run `php phing frontend-api-generate-graphql-schema` inside php-fpm container -2. Run `pnpm run gql:codegen` in storefront container +Storefront documentation is available in the documentation of Shopsys Platform in the section [Storefront](https://docs.shopsys.com/storefront). diff --git a/storefront/docs/authentication.md b/storefront/docs/authentication.md deleted file mode 100644 index fb7adb86a8..0000000000 --- a/storefront/docs/authentication.md +++ /dev/null @@ -1,63 +0,0 @@ -### Authentication - -#### Frontend API - -If you didn't do it previously, you have to generate private keys for Frontend API. - -```plain -php phing frontend-generate-new-keys -``` - -#### Authentication mechanism - -Authentication is performed via [@urql/auth-exchange](https://formidable.com/open-source/urql/docs/advanced/authentication). -Proper options for authExchange can be obtained with `getAuthExchangeOptions` from `urql/authExchange.ts` and it can be used for server-side rendering and client side requests. - -Following options are created for `authExchange`: - -`addAuthToOperation` -Responsible for adding access token as `Authorization: Bearer xxx` header to each request made with urql. -Authorization header is not added when no authState exists (no previously authenticated user) or when `RefreshTokens` mutation is performed. -Because when `RefreshToken` mutation is performed, access token can be already invalid and FE API blocks every request with invalid access token. - -`didAuthError` -Check whether error returned from API is an authentication error (e.g. HTTP response status code is 401). - -`getAuth` -This option is created with factory `createGetAuth`, so it's possible to pass `GetServerSidePropsContext` which is necessary for properly authenticated requests in SSR. -Initially, when no `authState` is stored in memory, tokens are loaded from persistent storage (cookies). -When `refreshToken` does not exist in persistent storage, request is then handled as anonymous. -When `accessToken` does not exist in persistent storage, a refresh token attempt is made immediately. -`getAuth` function is also invoked after `didAuthError` function return `true` (after authentication error in any request). -In that case `authState` is already present in memory and a refresh token attempt is made immediately. - -While the urql is refreshing tokens, all other calls are paused. -After successful refresh, previously forbidden requests are re-executed with the new access token. - -For logging the user in/out we can use `useAuth` hook. - -```plain -/hooks/auth/useAuth.tsx -``` - -User login - -```plain -const [[loginResult, login]] = useAuth(); - -login(email: string, password: string); -``` - -This function calls the API mutation with the provided email and password. -If everything is OK, user is logged in. -`accessToken` and `refreshToken` are then stored in the cookie and zustand state is updated with information the user is logged in. - -User logout - -```plain -const [, [, logout]] = useAuth(); - -logout(); -``` - -Zustand state is updated with information the user is no longer authenticated and tokens are deleted from cookie. diff --git a/storefront/docs/coding-standards.md b/storefront/docs/coding-standards.md deleted file mode 100644 index 78e04a0ef6..0000000000 --- a/storefront/docs/coding-standards.md +++ /dev/null @@ -1,27 +0,0 @@ -### Eslint -- can show you error on demand when you are writing your code - and I have to install editor plugin to use it (can be used on server side in any test) -- rules are defined in files: -```plain -- .eslintignore, .eslintrc.json -``` - -### Prettier -- can format you code on save or can be fired by key shortcut - and I have to install editor plugin to use it -- rules are defined in file: -```plain -- .prettierrc -``` - -### Editorconfig -- adds coding standards into your IDE even if you don't have any plugin installed -- rules are defined in file: -```plain -- .editorconfig -``` - -### Babel -- rules are defined in file: -```plain -- .babelrc - -``` diff --git a/storefront/docs/gtm/eventFactories.md b/storefront/docs/gtm/eventFactories.md deleted file mode 100644 index 6e5496cb91..0000000000 --- a/storefront/docs/gtm/eventFactories.md +++ /dev/null @@ -1,301 +0,0 @@ -# GTM Event Factories - -These factories are responsible for creating and preparing GTM event object. They can be written either as basic `get` methods, such as `getGtmCartViewEvent`, or as hooks, such as `useGtmStaticPageViewEvent`. The difference between them is - -## getGtmCartViewEvent - -### Event Factory Signature: - -```typescript -export const getGtmCartViewEvent = ( - currencyCode: string; // the code of the currency used on the domain - valueWithoutVat: number, // the value of the cart without VAT - valueWithVat: number, // the value of the cart with VAT - products: GtmCartItemType[] | undefined, // products in cart, if available -): GtmCartViewEventType => { - // function body not included in this code block -} -``` - -## getGtmContactInformationPageViewEvent - -### Event Factory Signature: - -```typescript -export const getGtmContactInformationPageViewEvent = ( - gtmCartInfo: GtmCartInfoType, // the cart of the current user in the shape of GTM cart information -): GtmContactInformationPageViewEventType => { - // function body not included in this code block -}; -``` - -## getGtmPaymentAndTransportPageViewEvent - -### Event Factory Signature: - -```typescript -export const getGtmPaymentAndTransportPageViewEvent = ( - currencyCode: string; // the code of the currency used on the domain - gtmCartInfo: GtmCartInfoType, // the cart of the current user in the shape of GTM cart information -): GtmPaymentAndTransportPageViewEventType => { - // function body not included in this code block -} -``` - -## getGtmPaymentFailEvent - -### Event Factory Signature: - -```typescript -export const getGtmPaymentFailEvent = ( - orderId: string, // ID of the order for which the payment has failed -): GtmPaymentFailEventType => { - // function body not included in this code block -}; -``` - -## getGtmCreateOrderEvent - -### Event Factory Signature: - -```typescript -export const getGtmCreateOrderEvent = ( - gtmCreateOrderEventOrderPart: GtmCreateOrderEventOrderPartType, // part of the GtmCreateOrderEvent object containing information about the order - gtmCreateOrderEventUserPart: GtmUserInfoType, // part of the GtmCreateOrderEvent object containing information about the user - isPaymentSuccessful?: boolean, // boolean pointer if the payment was successful, not filled if we cannot determine the payment status -): GtmCreateOrderEventType => { - // function body not included in this code block -}; -``` - -## getGtmCreateOrderEventOrderPart - -### Event Factory Signature: - -```typescript -export const getGtmCreateOrderEventOrderPart = ( - cart: CartFragmentApi, // the cart of the current user in the shape of basic CartFragment object - payment: SimplePaymentFragmentApi, // the payment method chosen by the user that will be used to pay for the order - promoCode: string | null, // promo code used for the order, if applicable - orderNumber: string, // identifying number of the order - reviewConsents: GtmReviewConsentsType, // information about consents previously given by the user - domainConfig: DomainConfigType, // configuration for the current domain -): GtmCreateOrderEventOrderPartType => { - // function body not included in this code block -}; -``` - -## getGtmCreateOrderEventUserPart - -### Event Factory Signature: - -```typescript -export const getGtmCreateOrderEventUserPart = ( - user: CurrentCustomerType | null | undefined, // information about current user - userContactInformation: ContactInformation, // contact information filled by the user during order -): GtmUserInfoType => { - // function body not included in this code block -}; -``` - -## getGtmSendFormEvent - -### Event Factory Signature: - -```typescript -export const getGtmSendFormEvent = ( - form: GtmFormType, // type of the form submitted by the user -): GtmSendFormEventType => { - // function body not included in this code block -}; -``` - -## getGtmProductClickEvent - -### Event Factory Signature: - -```typescript -export const getGtmProductClickEvent = ( - product: ListedProductFragmentApi | SimpleProductFragmentApi, // information about the product clicked by the user - gtmProductListName: GtmProductListNameType, // name of the list from which the product was clicked - listIndex: number, // index of the product within the list - domainUrl: string, // URL of the current domain -): GtmProductClickEventType => { - // function body not included in this code block -}; -``` - -## getGtmProductDetailViewEvent - -### Event Factory Signature: - -```typescript -export const getGtmProductDetailViewEvent = ( - product: ProductDetailFragmentApi | MainVariantDetailFragmentApi, // information about the product displayed on on the product detail page - currencyCode: string; // the code of the currency used on the domain - domainUrl: string, // URL of the current domain -): GtmProductDetailViewEventType => { - // function body not included in this code block -} -``` - -## getGtmAutocompleteResultsViewEvent - -### Event Factory Signature: - -```typescript -export const getGtmAutocompleteResultsViewEvent = ( - searchResult: AutocompleteSearchQueryApi, // object with all autocomplete search results - keyword: string, // keyword for which the results were returned -): GtmAutocompleteResultsViewEventType => { - // function body not included in this code block -}; -``` - -## getGtmAutocompleteResultClickEvent - -### Event Factory Signature: - -```typescript -export const getGtmAutocompleteResultClickEvent = ( - keyword: string, // keyword for which the results were returned - section: GtmSectionType, // type of the section of the autocomplete results on which the user clicked - itemName: string, // name of the autocomplete search results item clicked by the user -): GtmAutocompleteResultClickEventType => { - // function body not included in this code block -}; -``` - -## useGtmStaticPageViewEvent - -### Event Factory Signature: - -```typescript -export const useGtmStaticPageViewEvent = ( - pageType: GtmPageType, // type of the page viewed by the user - breadcrumbs?: BreadcrumbFragmentApi[], // breadcrumbs for the viewed page, if available -): GtmPageViewEventType => { - // function body not included in this code block -}; -``` - -## useGtmFriendlyPageViewEvent - -### Event Factory Signature: - -```typescript -export const useGtmFriendlyPageViewEvent = ( - friendlyUrlPageData: FriendlyUrlPageType | null | undefined, // data for the friendly URL page -): GtmPageViewEventType => { - // function body not included in this code block -}; -``` - -## getGtmPageViewEvent - -### Event Factory Signature: - -```typescript -export const getGtmPageViewEvent = ( - pageInfo: GtmPageInfoType, // information about the viewed page - gtmCartInfo: GtmCartInfoType | null, // the cart of the current user in the shape of GTM cart information, if available - isCartLoaded: boolean, // boolean pointer saying if the cart is loaded - user: CurrentCustomerType | null | undefined, // information about the current user - userContactInformation: ContactInformation, // contact information filled by the user during order - domainConfig: DomainConfigType, // config for the current domain -): GtmPageViewEventType => { - // function body not included in this code block -}; -``` - -## getGtmChangeCartItemEvent - -### Event Factory Signature: - -```typescript -export const getGtmChangeCartItemEvent = ( - event: GtmEventType.add_to_cart | GtmEventType.remove_from_cart, // type of the event saying if we are removing or adding items to cart - cartItem: CartItemFragmentApi, // removed (or added) cart item - listIndex: number | undefined, // list index from which the item was removed/added, it can be index within cart (for removing) or index within any other list (for adding) - quantity: number, // how much was removed/added, absolute value of the delta of the previous and current quantity - currencyCode: string; // the code of the currency used on the domain - eventValueWithoutVat: number, // value of the event without VAT, calculated as product price without VAT * quantity - eventValueWithVat: number, // value of the event with VAT, calculated as product price with VAT * quantity - gtmProductListName: GtmProductListNameType, // name of the list from which the product was removed/added - domainUrl: string, // URL of the current domain - gtmCartInfo?: GtmCartInfoType | null, // the cart of the current user in the shape of GTM cart information, if available -): GtmChangeCartItemEventType => { - // function body not included in this code block -} -``` - -## getGtmPaymentChangeEvent - -### Event Factory Signature: - -```typescript -export const getGtmPaymentChangeEvent = ( - gtmCartInfo: GtmCartInfoType, // the cart of the current user in the shape of GTM cart information, if available - updatedPayment: SimplePaymentFragmentApi, // payment method newly updated by the user -): GtmPaymentChangeEventType => { - // function body not included in this code block -}; -``` - -## getGtmTransportChangeEvent - -### Event Factory Signature: - -```typescript -export const getGtmTransportChangeEvent = ( - gtmCartInfo: GtmCartInfoType, // the cart of the current user in the shape of GTM cart information, if available - updatedTransport: TransportWithAvailablePaymentsAndStoresFragmentApi, // transport method newly updated by the user - updatedPickupPlace: ListedStoreFragmentApi | null, // pickup place method newly updated by the user, if available - paymentName: string | undefined, // name of the selected payment method -): GtmTransportChangeEventType => { - // function body not included in this code block -}; -``` - -## getGtmProductListViewEvent - -### Event Factory Signature: - -```typescript -export const getGtmProductListViewEvent = ( - products: ListedProductFragmentApi[], // products contained in the viewed list - gtmProductListName: GtmProductListNameType, // name of the viewed list - currentPage: number, // current page of the viewed list - pageSize: number, // page size of the viewed list - domainUrl: string, // URL of the current domain -): GtmProductListViewEventType => { - // function body not included in this code block -}; -``` - -## getGtmShowMessageEvent - -### Event Factory Signature: - -```typescript -export const getGtmShowMessageEvent = ( - type: GtmMessageType, // type of the message shown to the user (e.g. error, information) - message: string, // content of the message shown to the user - detail: GtmMessageDetailType | string, // any additional information about the message shown to the user, can be of a predefined type GtmMessageDetailType or any arbitrary string value - origin?: GtmMessageOriginType, // origin of the message, saying which part of the website triggered it -): GtmShowMessageEventType => { - // function body not included in this code block -}; -``` - -## getGtmConsentUpdateEvent - -### Event Factory Signature: - -```typescript -export const getGtmConsentUpdateEvent = ( - updatedGtmConsentInfo: GtmConsentInfoType, // newly updated consents given by the user -): GtmConsentUpdateEventType => { - // function body not included in this code block -}; -``` diff --git a/storefront/docs/gtm/eventHandlers.md b/storefront/docs/gtm/eventHandlers.md deleted file mode 100644 index c8edd84540..0000000000 --- a/storefront/docs/gtm/eventHandlers.md +++ /dev/null @@ -1,12 +0,0 @@ -# GTM Event Handlers - -These methods are responsible for accepting parameters, build a GTM event object and push it to the data layer. However, they outsource both these tasks to 2 methods: - -- event factories (such as `getGtmPaymentFailEvent`) -- `gtmSafePushEvent`, which: - - makes sure the event can be safely pushed to the data layer - - pushed the event to the data layer - -Because of that, these methods are rather simple and do not require detailed documentation. Their only role is to handle events using other methods, sometimes serving as guards to check for nullability of some arguments. - -For the description of **accepted parameters**, see corresponding event factory methods. diff --git a/storefront/docs/gtm/events.md b/storefront/docs/gtm/events.md deleted file mode 100644 index 9b5724a877..0000000000 --- a/storefront/docs/gtm/events.md +++ /dev/null @@ -1,418 +0,0 @@ -# GTM Event Objects - -These objects represent all GTM events. They are composed from GtmEventInterface, GtmEventType, and the content of the event. Each event has a description of all its properties. - -## GtmEventInterface - -This interface is a generic type that includes an event of type EventType and the property \_clear of boolean type. The remaining properties are of type EventContent, which is a type argument through which one can extend this generic interface. - -### Event Object Properties: - -```typescript -export type GtmEventInterface = { - event: EventType; // the type of the event - _clear: boolean; // lears the current data layer -} & EventContent; // additional properties of the event; -``` - -## GtmPageViewEventType - -This type is used for page view tracking. - -### Event Object Properties: - -```typescript -export type GtmPageViewEventType = GtmEventInterface< - GtmEventType.page_view, // the type of the event - { - language: string; // the current language of the domain - currencyCode: string; // the code of the currency used on the domain - consent: GtmConsentInfoType; // information about user consent - page: GtmPageInfoType; // information about the viewed page - user: GtmUserInfoType; // information about the user - device: GtmDeviceTypes; // information about the user's device - cart: GtmCartInfoType | null; // information about the user's cart, if available - _isLoaded: boolean; // indicates if the page has finished loading - } ->; -``` - -## GtmConsentUpdateEventType - -This type is used for consent update tracking. - -### Event Object Properties: - -```typescript -export type GtmConsentUpdateEventType = GtmEventInterface< - GtmEventType.consent_update, // the type of the event - { - consent: GtmConsentInfoType; // information about user consent - } ->; -``` - -## GtmChangeCartItemEventType - -This type is used for tracking both adding and removing cart items. - -### Event Object Properties (all are inherited): - -```typescript -export type GtmChangeCartItemEventType = GtmAddToCartEventType | GtmRemoveFromCartEventType; -``` - -## GtmAddToCartEventType - -This type is used for tracking adding items to a cart. Keep in mind that adding does not need to mean the product has not been added before. It can also mean increasing product's quantity. - -### Event Object Properties: - -```typescript -export type GtmAddToCartEventType = GtmEventInterface< - GtmEventType.add_to_cart, // the type of the event - { - ecommerce: { - listName: GtmProductListNameType; // the name of the product list from which the product was added - currencyCode: string; // the code of the currency used on the domain - valueWithoutVat: number; // the value of the added products without VAT (value of the event) - valueWithVat: number; // the value of the added products with VAT (value of the event) - products: GtmCartItemType[] | undefined; // information about the products added to the cart, if available - }; - cart?: GtmCartInfoType | null; // information about the user's cart, if available - } ->; -``` - -## GtmRemoveFromCartEventType - -This type is used for tracking removing items from a cart. - -### Event Object Properties: - -```typescript -export type GtmRemoveFromCartEventType = GtmEventInterface< - GtmEventType.remove_from_cart, // the type of the event - { - ecommerce: { - listName: GtmProductListNameType; // the name of the product list from which the product was removed - currencyCode: string; // the code of the currency used on the domain - valueWithoutVat: number; // the value of the removed products without VAT (value of the event) - valueWithVat: number; // the value of the removed products with VAT (value of the event) - products: GtmCartItemType[] | undefined; // information about the products removed from the cart, if available - }; - cart?: GtmCartInfoType | null; // information about the user's cart, if available - } ->; -``` - -## GtmCartViewEventType - -This type is used for tracking cart view events. - -### Event Object Properties: - -```typescript -export type GtmCartViewEventType = GtmEventInterface< - GtmEventType.cart_view, // the type of the event - { - ecommerce: { - currencyCode: string; // the code of the currency used on the domain - valueWithoutVat: number; // the value of the products in the cart without VAT - valueWithVat: number; // the value of the products in the cart with VAT - products: GtmCartItemType[] | undefined; // information about the products in the cart, if available - }; - } ->; -``` - -## GtmProductListViewEventType - -This type is used for tracking when a user views a list of products. - -### Event Object Properties: - -```typescript -export type GtmProductListViewEventType = GtmEventInterface< - GtmEventType.product_list_view, // the type of the event - { - ecommerce: { - listName: GtmProductListNameType; // the name of the product list - products: GtmListedProductType[] | undefined; // an array of product information objects for the listed products, if available - }; - } ->; -``` - -## GtmProductClickEventType - -This type is used for tracking when a user clicks on a product and is redirected to product detail page. - -### Event Object Properties: - -```typescript -export type GtmProductClickEventType = GtmEventInterface< - GtmEventType.product_click, // the type of the event - { - ecommerce: { - listName: GtmProductListNameType; // the name of the product list where the product was clicked - products: GtmListedProductType[] | undefined; // an array containing a single object of product information object for the clicked product, if available - }; - } ->; -``` - -## GtmProductDetailViewEventType - -This type is used for tracking when a user views the details of a product. - -### Event Object Properties: - -```typescript -export type GtmProductDetailViewEventType = GtmEventInterface< - GtmEventType.product_detail_view, // the type of the event - { - ecommerce: { - currencyCode: string; // the code of the currency used on the domain - valueWithoutVat: number; // the price of the viewed product without VAT (value of the event) - valueWithVat: number; // the price of the viewed product with VAT (value of the event) - products: GtmProductInterface[] | undefined; // an array containing a single object of product information object for the viewed product, if available - }; - } ->; -``` - -## GtmPaymentAndTransportPageViewEventType - -This type is used for tracking when a user views the payment and transport page. - -### Event Object Properties: - -```typescript -export type GtmPaymentAndTransportPageViewEventType = GtmEventInterface< - GtmEventType.payment_and_transport_page_view, // the type of the event - { - ecommerce: { - currencyCode: string; // the code of the currency used on the domain - valueWithoutVat: number; // the total value of the products in the cart without VAT - valueWithVat: number; // the total value of the products in the cart with VAT - products: GtmCartItemType[] | undefined; // an array of product information objects for the products in the cart, if available - }; - } ->; -``` - -## GtmAutocompleteResultsViewEventType - -This type is used for tracking when a user views autocomplete search results. - -### Event Object Properties: - -```typescript -export type GtmAutocompleteResultsViewEventType = GtmEventInterface< - GtmEventType.autocomplete_results_view, // the type of the event - { - autocompleteResults: { - keyword: string; // the search keyword entered by the user - results: number; // the number of search results displayed to the user - sections: { [key in GtmSectionType]: number }; // an object that maps each section of search results to the number of results in that particular section - }; - } ->; -``` - -## GtmAutocompleteResultClickEventType - -This type is used for tracking clicks on autocomplete results. - -### Event Object Properties: - -```typescript -export type GtmAutocompleteResultClickEventType = GtmEventInterface< - GtmEventType.autocomplete_result_click, // the type of the event - { - autocompleteResultClick: { - section: GtmSectionType; // the section of the autocomplete results where the result was clicked - itemName: string; // the name of the item clicked on - keyword: string; // the search keyword used to generate the autocomplete results - }; - } ->; -``` - -## GtmTransportChangeEventType - -This type is used for tracking changes to the transport option during checkout. - -### Event Object Properties: - -```typescript -export type GtmTransportChangeEventType = GtmEventInterface< - GtmEventType.transport_change, // the type of the event - { - ecommerce: { - currencyCode: string; // the code of the currency used on the domain - valueWithoutVat: number; // the value of the products in the cart without VAT - valueWithVat: number; // the value of the products in the cart with VAT - promoCodes: string[] | undefined; // an array of promo codes applied to the order, if any - paymentType: string | undefined; // the payment type selected by the user, if any - transportPriceWithoutVat: number; // the price of the transport without VAT - transportPriceWithVat: number; // the price of the transport with VAT - transportType: string; // the type of transport selected by the user - transportDetail: string; // any additional details about the transport selected by the user - transportExtra: string[]; // an array of any extra transport-related information provided by the user - products: GtmCartItemType[]; // information about the products in the cart - }; - } ->; -``` - -## GtmContactInformationPageViewEventType - -This type is used for tracking when a user views the contact information page during checkout. - -### Event Object Properties: - -```typescript -export type GtmContactInformationPageViewEventType = GtmEventInterface< - GtmEventType.contact_information_page_view, // the type of the event - { - ecommerce: { - currencyCode: string; // the code of the currency used on the domain - valueWithoutVat: number; // the value of the products in the cart without VAT - valueWithVat: number; // the value of the products in the cart with VAT - promoCodes: string[] | undefined; // an array of promo codes applied to the order, if any - products: GtmCartItemType[] | undefined; // information about the products in the cart, if available - }; - } ->; -``` - -## GtmPaymentChangeEventType - -This type is used for tracking changes to the payment option during checkout. - -### Event Object Properties: - -```typescript -export type GtmPaymentChangeEventType = GtmEventInterface< - GtmEventType.payment_change, // the type of the event - { - ecommerce: { - currencyCode: string; // the code of the currency used on the domain - valueWithoutVat: number; // the value of the products in the cart without VAT - valueWithVat: number; // the value of the products in the cart with VAT - promoCodes: string[] | undefined; // an array of promo codes applied to the order, if any - paymentType: string; // the payment type selected by the user - paymentPriceWithoutVat: number; // the price of the payment without VAT - paymentPriceWithVat: number; // the price of the payment with VAT - products: GtmCartItemType[] | undefined; // information about the products in the cart, if available - }; - } ->; -``` - -## GtmPaymentFailEventType - -This type is used for tracking when a payment fails. - -### Event Object Properties: - -```typescript -export type GtmPaymentFailEventType = GtmEventInterface< - GtmEventType.payment_fail, // the type of the event - { - paymentFail: { - id: string; // the ID of the order for which the payment failed - }; - } ->; -``` - -## GtmCreateOrderEventOrderPartType - -This type represents the order part of the GtmCreateOrderEventType. It is stored in local storage before redirecting to payment gate, if the user has chosen to pay using a payment that requires a redirect. - -### Event Object Properties: - -```typescript -export type GtmCreateOrderEventOrderPartType = { - currencyCode: string; // the code of the currency used on the domain - id: string; // the ID of the order - valueWithoutVat: number; // the value of the products in the order without VAT - valueWithVat: number; // the value of the products in the order with VAT - vatAmount: number; // the total amount of VAT paid for the order - paymentPriceWithoutVat: number; // the price of the payment without VAT - paymentPriceWithVat: number; // the price of the payment with VAT - promoCodes: string[] | undefined; // any promo codes used for the order, if available - discountAmount: number | undefined; // the total amount of discounts applied to the order, if available - paymentType: string; // the type of payment used for the order - reviewConsents: GtmReviewConsentsType; // user's review consents - products: GtmCartItemType[] | undefined; // information about the products in the order, if available -}; -``` - -## GtmPurchaseEventPaymentPartType - -This type represents the payment part of the GtmCreateOrderEventType. It is never stored in local storage, but always set when an order is created according to the current information about the payment. - -### Event Object Properties: - -```typescript -export type GtmPurchaseEventPaymentPartType = { - isPaymentSuccessful: boolean | undefined; // whether the payment was successful or not, if available -}; -``` - -## GtmCreateOrderEventType - -This type is used for tracking when an order is created by the user. It consists of the GtmCreateOrderEventOrderPartType, GtmPurchaseEventPaymentPartType, and GtmUserInfoType objects. - -### Event Object Properties: - -```typescript -export type GtmCreateOrderEventType = GtmEventInterface< - GtmEventType.create_order, // the type of the event - { - ecommerce: GtmCreateOrderEventOrderPartType & GtmPurchaseEventPaymentPartType; - user: GtmUserInfoType - } - -``` - -## GtmShowMessageEventType - -This type is used for tracking when a message is displayed to the user. - -### Event Object Properties: - -```typescript -export type GtmShowMessageEventType = GtmEventInterface< - GtmEventType.show_message, // the type of the event - { - eventParameters: { - type: GtmMessageType; // the type of the message displayed - origin: GtmMessageOriginType | undefined; // the origin of the message, if available - detail: string | undefined; // any additional details about the message, if available - message: string; // the message displayed to the user - }; - } ->; -``` - -## GtmSendFormEventType - -This type is used for tracking when a form is submitted by the user. - -### Event Object Properties: - -```typescript -export type GtmSendFormEventType = GtmEventInterface< - GtmEventType.send_form, // the type of the event - { - eventParameters: { - form: GtmFormType; // information about the form submitted by the user - }; - } ->; -``` diff --git a/storefront/docs/gtm/gtm.md b/storefront/docs/gtm/gtm.md deleted file mode 100644 index 95ad9cf6c8..0000000000 --- a/storefront/docs/gtm/gtm.md +++ /dev/null @@ -1,246 +0,0 @@ -# GTM Core Functions and Helpers - -These functions and helpers are responsible for some of the main logic in GTM. It is in these functions where things like: - -- cart mapping -- user information mapping -- page information mapping - are happening. - -It is very likely, that if your project requires customization of the logic, this is the file where the modifications are going to take place. Below you can see descriptions of almost all functions. Only self-descriptive and primitive functions are omitted. - -## useGtmCartInfo - -Hook used to allow the application to work with cart information mapped according to the GTM requirements. It is a hook wrapper of the underlying `getGtmMappedCart` function, which takes care of the mapping itself. The hook handles the default state where no cart is available - -### Hook Signature: - -```typescript -export const useGtmCartInfo = (): { gtmCartInfo: GtmCartInfoType | null; isCartLoaded: boolean } => { - // code omitted for simplification - - return useMemo( - () => { - if ((cartUuid === null && !isUserLoggedIn) || cart === null) { - return { gtmCartInfo: null, isCartLoaded: !isFetching }; - } - - return { - gtmCartInfo: getGtmMappedCart(cart, promoCode, isUserLoggedIn, domain, cartUuid), - isCartLoaded: !isFetching, - }; - }, - [ - // code omitted for simplification - ], - ); -}; -``` - -## getGtmMappedCart - -Function used to map the cart information to be suitable for GTM. Uses other helpers to get different parts of the mapped object. - -### Method Signature: - -```typescript -export const getGtmMappedCart = ( - cart: CartFragmentApi, - promoCode: string | null, - isUserLoggedIn: boolean, - domain: DomainConfigType, - cartUuid: string | null, -): GtmCartInfoType => { - // function body omitted out for simplification -}; -``` - -## getAbandonedCartUrl - -Function used to generate an abandoned cart URL. - -### Method Signature: - -```typescript -const getAbandonedCartUrl = (isUserLoggedIn: boolean, domain: DomainConfigType, cartUuid: string | null) => { - if (isUserLoggedIn) { - const [loginRelativeUrl, cartRelativeUrl] = getInternationalizedStaticUrls(['/login', '/cart'], domain.url); - - return domain.url + getStringWithoutLeadingSlash(loginRelativeUrl) + '?r=' + cartRelativeUrl; - } - - const [abandonedCartRelativeUrl] = getInternationalizedStaticUrls( - [{ url: '/abandoned-cart/:cartUuid', param: cartUuid }], - domain.url, - ); - - return domain.url + getStringWithoutLeadingSlash(abandonedCartRelativeUrl); -}; -``` - -## getAbandonedCartUrl - -Function used to create page info objects for various entities which can be displayed on the friendly URL page. It handles special cases, such as category detail, blog article detail, brand detail, and so on. If for some reason page type cannot be deterimed, it returns a default page info object. - -### Method Signature: - -```typescript -export const getGtmPageInfoTypeForFriendlyUrl = ( - friendlyUrlPageData: FriendlyUrlPageType | null | undefined, -): GtmPageInfoType => { - let pageInfo = getGtmPageInfoType(GtmPageType.not_found, friendlyUrlPageData?.breadcrumb); - - if (friendlyUrlPageData === undefined) { - return pageInfo; - } - - switch ( - friendlyUrlPageData?.__typename - // code omitted for simplification - ) { - } - - return pageInfo; -}; -``` - -## getPageInfoForCategoryDetailPage, getPageInfoForBlogArticleDetailPage, getPageInfoForBrandDetailPage - -Helper functions used to generate specific properties for the friendly URL page, if the displayed entity is of type: - -- category -- blog article -- brand - -### Method Signatures: - -```typescript -const getPageInfoForCategoryDetailPage = ( - defaultPageInfo: GtmPageInfoInterface, - categoryDetailData: CategoryDetailFragmentApi, -): GtmCategoryDetailPageInfoType => ({ - // function body omitted for simplification -}); - -const getPageInfoForBlogArticleDetailPage = ( - defaultPageInfo: GtmPageInfoType, - blogArticleDetailData: BlogArticleDetailFragmentApi, -): GtmBlogArticleDetailPageInfoType => ({ - // function body omitted for simplification -}); - -const getPageInfoForBrandDetailPage = ( - defaultPageInfo: GtmPageInfoType, - brandDetailData: BrandDetailFragmentApi, -): GtmBrandDetailPageInfoType => ({ - // function body omitted for simplification -}); -``` - -## gtmSafePushEvent - -Essential function used to push all event to the data layer. - -### Method Signature: - -```typescript -export const gtmSafePushEvent = (event: GtmEventInterface): void => { - if (canUseDom()) { - window.dataLayer = window.dataLayer ?? []; - window.dataLayer.push(event); - } -}; -``` - -## getGtmUserInfo - -Basic function used to get the user information in a GTM-suitable format. It takes care of dispatching the initial creation based on the contact information from order and it also handles overwriting of these information with credentials of a logged-in customer. - -### Method Signature: - -```typescript -export const getGtmUserInfo = ( - currentSignedInCustomer: CurrentCustomerType | null | undefined, - userContactInformation: ContactInformation, -): GtmUserInfoType => { - const userInfo: GtmUserInfoType = getGtmUserInfoForVisitor(userContactInformation); - - if (currentSignedInCustomer) { - overwriteGtmUserInfoWithLoggedCustomer(userInfo, currentSignedInCustomer, userContactInformation); - } - - return userInfo; -}; -``` - -## getGtmUserInfoForVisitor - -Gets the basic information about the user. Because of the fact that untouched fields from the contact information form are not stored as null or undefined, but as empty string, it must check for the length of the string and only include the datapoint if it is really filled. - -### Method Signature: - -```typescript -const getGtmUserInfoForVisitor = (userContactInformation: ContactInformation) => ({ - status: GtmUserStatus.visitor, - ...(userContactInformation.city.length > 0 && { city: userContactInformation.city }), - // code omitted for simplification - type: getGtmUserType(userContactInformation.customer), -}); -``` - -## getGtmUserType - -Method used to help differentiate between B2B and B2C customers. Prepared for further extension of the logic. - -### Method Signature: - -```typescript -const getGtmUserType = (customerType: CustomerTypeEnum | undefined): GtmUserType | undefined => { - if (customerType === undefined) { - return undefined; - } - - if (customerType === CustomerTypeEnum.CompanyCustomer) { - return GtmUserType.b2b; - } - - return GtmUserType.b2c; -}; -``` - -## overwriteGtmUserInfoWithLoggedCustomer - -Method used to overwrite default information about the customer with the information from his account. Here the logic is following: - -- `status`, `id`, and `group` are always overwritten -- other properties are only overwritten, if they haven't been filled before -- `type` is filled in based on the previous value of the field and on the value currently signed-in customer - -### Method Signature: - -```typescript -const overwriteGtmUserInfoWithLoggedCustomer = ( - userInfo: GtmUserInfoType, - currentSignedInCustomer: CurrentCustomerType, - userContactInformation: ContactInformation, -) => { - userInfo.status = GtmUserStatus.customer; - userInfo.id = currentSignedInCustomer.uuid; - userInfo.group = currentSignedInCustomer.pricingGroup; - - if (userInfo.street === undefined || userInfo.street.length === 0) { - userInfo.street = currentSignedInCustomer.street; - } - // code omitted for simplification - - if (userInfo.type !== undefined) { - return; - } - - if (currentSignedInCustomer.companyCustomer) { - userInfo.type = GtmUserType.b2b; - } else { - userInfo.type = GtmUserType.b2c; - } -}; -``` diff --git a/storefront/docs/gtm/hooks.md b/storefront/docs/gtm/hooks.md deleted file mode 100644 index 3060782f3e..0000000000 --- a/storefront/docs/gtm/hooks.md +++ /dev/null @@ -1,124 +0,0 @@ -# GTM Event Hooks - -These hooks are responsible for handling of asynchronous GTM events according to how React's lifecycle works. They do not return anything, just handle the asynchronous event. They are usually used for events such as page view or list view. - -## useGtmPaginatedProductListViewEvent - -Hook used to handle viewing of a paginated products list, e.g. category detail, brand detail, or flag detail. -It is triggered every time page (and therefore products) change. - -### Hook Signature: - -```typescript -export const useGtmPaginatedProductListViewEvent = ( - paginatedProducts: ListedProductFragmentApi[] | undefined, // array of displayed products, if loaded and available - gtmProductListName: GtmProductListNameType, // name of the viewed paginated list -): void => { - // function body not included in this code block -}; -``` - -## useGtmSliderProductListViewEvent - -Hook used to handle viewing of a products list inside a slider, such as promoted products on homepage. -It is triggered every time page therefore products change. - -### Hook Signature: - -```typescript -export const useGtmSliderProductListViewEvent = ( - products: ListedProductFragmentApi[] | undefined, // array of displayed products, if loaded and available - gtmProuctListName: GtmProductListNameType, // name of the viewed paginated list -): void => { - // function body not included in this code block -}; -``` - -## useGtmAutocompleteResultsViewEvent - -Hook used to handle viewing of autocomplete search results. It is triggered every time the search keyword changes. - -### Hook Signature: - -```typescript -export const useGtmAutocompleteResultsViewEvent = ( - searchResult: AutocompleteSearchQueryApi | undefined, // object containing all autocomplete search results, if loaded and available - keyword: string, // search keyword for which the results were found - fetching: boolean, // boolean pointer saying if the results are still loading -): void => { - // function body not included in this code block -}; -``` - -## useGtmPageViewEvent - -Basic hook used to handle viewing of a page. It is sometimes accompanied with one of the hooks below if the page is of a special type. The parameter used for this hook can be taken from `useGtmStaticPageViewEvent` or `useGtmFriendlyPageViewEvent` based on the type of the page ('static' vs 'friendly URL'). - -### Hook Signature: - -```typescript -export const useGtmPageViewEvent = ( - gtmPageViewEvent: GtmPageViewEventType, // object containing information about the viewed page - fetching?: boolean, // boolean pointer saying if the results are still loading -): void => { - // function body not included in this code block -}; -``` - -## useGtmCartViewEvent - -Hook used to handle viewing of the cart page. The parameter used for this hook can be taken from `useGtmStaticPageViewEvent`. - -### Hook Signature: - -```typescript -export const useGtmCartViewEvent = ( - gtmPageViewEvent: GtmPageViewEventType, // object containing information about the viewed page -): void => { - // function body not included in this code block -}; -``` - -## useGtmContactInformationPageViewEvent - -Hook used to handle viewing of the contact information page. The parameter used for this hook can be taken from `useGtmStaticPageViewEvent`. - -### Hook Signature: - -```typescript -export const useGtmContactInformationPageViewEvent = ( - gtmPageViewEvent: GtmPageViewEventType, // object containing information about the viewed page -): void => { - // function body not included in this code block -}; -``` - -## useGtmPaymentAndTransportPageViewEvent - -Hook used to handle viewing of the transport and payment page. The parameter used for this hook can be taken from `useGtmStaticPageViewEvent`. - -### Hook Signature: - -```typescript -export const useGtmPaymentAndTransportPageViewEvent = ( - gtmPageViewEvent: GtmPageViewEventType, // object containing information about the viewed page -): void => { - // function body not included in this code block -}; -``` - -## useGtmProductDetailViewEvent - -Hook used to handle viewing of the product detail page. - -### Hook Signature: - -```typescript -export const useGtmProductDetailViewEvent = ( - productDetailData: ProductDetailFragmentApi | MainVariantDetailFragmentApi, // information about the displayed product - slug: string, // slug of the page - fetching: boolean, // boolean pointer saying if the results are still loading -): void => { - // function body not included in this code block -}; -``` diff --git a/storefront/docs/gtm/objects.md b/storefront/docs/gtm/objects.md deleted file mode 100644 index 44c44f2510..0000000000 --- a/storefront/docs/gtm/objects.md +++ /dev/null @@ -1,206 +0,0 @@ -# GTM Objects - -These objects are used all across the GTM module for representation of various objects mapped according to what the data layer requires. - -## GtmReviewConsentsType - -Object representing user consent for tracking of different third-party services. Contains boolean properties seznam, google, and heureka to indicate if the user has consented to tracking by these services. - -### Object Properties: - -```typescript -export type GtmReviewConsentsType = { - seznam: boolean; // boolean pointer saying if the user has consented to Seznam tracking - google: boolean; // boolean pointer saying if the user has consented to Google tracking - heureka: boolean; // boolean pointer saying if the user has consented to Heureka tracking -}; -``` - -## GtmPageInfoInterface - -Interface representing basic information about a web page. - -### Object Properties: - -```typescript -export type GtmPageInfoInterface = ExtendedPageProperties & { - type: PageType; // type of the page, can be GtmPageType.category_detail or GtmPageType.seo_category_detail, GtmPageType.blog_article_detail or GtmPageType.brand_detail - pageId: string; // unique identifier of the page - breadcrumbs: BreadcrumbFragmentApi[]; // breadcrumbs to the current page -}; -``` - -## GtmCategoryDetailPageInfoType - -Object representing additional information about a web page that displays a category or subcategory. Extends the GtmPageInfoInterface. - -### Object Properties: - -```typescript -export type GtmCategoryDetailPageInfoType = GtmPageInfoInterface< - GtmPageType.category_detail | GtmPageType.seo_category_detail, // type of the page, can be either GtmPageType.category_detail or GtmPageType.seo_category_detail - { - category: string[]; // array of strings representing the category hierarchy of the page - categoryId: number[]; // array of category IDs representing the category hierarchy of the page - } ->; -``` - -## GtmBlogArticleDetailPageInfoType - -Object representing additional information about a web page that displays a blog article. Extends the GtmPageInfoInterface. - -### Object Properties: - -```typescript -export type GtmBlogArticleDetailPageInfoType = GtmPageInfoInterface< - GtmPageType.blog_article_detail, // type of the page, should always be GtmPageType.blog_article_detail - { - articleId: number; // unique identifier of the blog article - } ->; -``` - -## GtmBrandDetailPageInfoType - -Object representing additional information about a web page that displays a brand or manufacturer. Extends the GtmPageInfoInterface. - -### Object Properties: - -```typescript -export type GtmBrandDetailPageInfoType = GtmPageInfoInterface< - GtmPageType.brand_detail, // type of the page, should always be GtmPageType.brand_detail - { - brandId: number; // unique identifier of the brand - } ->; -``` - -## GtmPageInfoType - -Union type representing all possible types of web pages. Can be either GtmCategoryDetailPageInfoType, GtmBlogArticleDetailPageInfoType, GtmBrandDetailPageInfoType, or GtmPageInfoInterface. - -### Object Properties: - -```typescript -export type GtmPageInfoType = - | GtmCategoryDetailPageInfoType // page information for category detail or SEO category detail pages - | GtmBlogArticleDetailPageInfoType // page information for blog article detail pages - | GtmBrandDetailPageInfoType // page information for brand detail pages - | GtmPageInfoInterface; // basic page information without additional properties -``` - -## GtmCartInfoType - -Object representing information about a user's cart. - -### Object Properties: - -```typescript -export type GtmCartInfoType = { - abandonedCartUrl: string | undefined; // URL of the cart which can be used for recovery of an abandoned cart, optional - currencyCode: string; // the code of the currency used on the domain - valueWithoutVat: number; // total value of the cart without VAT - valueWithVat: number; // total value of the cart with VAT - products: GtmCartItemType[] | undefined; // array of products in the cart, if available - promoCodes?: string[]; // array of promo codes applied to the cart, optional -}; -``` - -## GtmUserInfoType - -Object representing information about a user. - -### Object Properties: - -```typescript -export type GtmUserInfoType = { - id?: string; // ID of the user, optional - email?: string; // email of the user, optional - emailHash?: string; // SHA256 hashed email of the user, optional - firstName?: string; // first name of the user, optional - lastName?: string; // last name of the user, optional - telephone?: string; // telephone number of the user, optional - street?: string; // street address of the user, optional - city?: string; // city of the user, optional - postcode?: string; // postal code of the user, optional - country?: string; // country of the user, optional - type?: GtmUserType; // type of the user (e.g. B2B or B2C), optional - status: GtmUserStatus; // status of the user (e.g. visitor or customer) - group?: string; // group of the user, optional -}; -``` - -## GtmConsentInfoType - -Object representing a user's consent for tracking in different categories. - -### Object Properties: - -```typescript -export type GtmConsentInfoType = { - statistics: GtmConsent; // user consent status for statistics tracking - marketing: GtmConsent; // user consent status for marketing tracking - preferences: GtmConsent; // user consent status for preference tracking -}; -``` - -## GtmProductInterface - -Interface representing information about a product. Is extended for specific cases of products, such as listed products or cart items. - -### Object Properties: - -```typescript -export type GtmProductInterface = { - id: number; // product ID - name: string; // product name - availability: string; // product availability status - flags: string[]; // array of product flags - priceWithoutVat: number; // product price without VAT - priceWithVat: number; // product price with VAT - vatAmount: number; // VAT amount - sku: string; // product catalog number - url: string; // product URL - brand: string; // product brand name - categories: string[]; // array of product categories - imageUrl?: string; // optional product image URL -}; -``` - -## GtmListedProductType - -Type that extends GtmProductInterface and adds an optional listIndex property, which represents the index of the product in a list (such as search results). - -### Object Properties: - -```typescript -export type GtmListedProductType = GtmProductInterface & { - listIndex?: number; // index of the product in a list (e.g., search results) -}; -``` - -## GtmCartItemType - -Type that extends GtmListedProductType and adds a required quantity property, which represents the quantity of the product in the cart. GtmListedProductType, as described above, extends GtmProductInterface and adds an optional listIndex property. Therefore, GtmCartItemType includes all properties defined in GtmProductInterface, GtmListedProductType, and adds the quantity property. - -### Object Properties: - -```typescript -export type GtmCartItemType = GtmListedProductType & { - quantity: number; // product quantity in the cart -}; -``` - -## GtmShippingInfoType - -Type that represents extra transport details. - -### Object Properties: - -```typescript -export type GtmShippingInfoType = { - transportDetail: string; // transport method details - transportExtra: string[]; // array of extra transport details -}; -``` diff --git a/storefront/docs/image.md b/storefront/docs/image.md deleted file mode 100644 index 678a1073ec..0000000000 --- a/storefront/docs/image.md +++ /dev/null @@ -1,42 +0,0 @@ -## Image -UI component to show images served by API with correct sizes on different devices. - -### Components props -- **image** - ImageSizesFragmentApi - nullable, property served from API -- **alt** - string - alternative text for image -- **type** - string - size variant of image according to images.yaml (example) -- **loading** - optional - html loading attribute to specific image loading behavior (auto, lazy, eager) -- **testId** - optional - string, used for testing - -### Code example -```yaml -images.yaml - -... -- name: transport - class: Shopsys\FrameworkBundle\Model\Transport\Transport - sizes: - - name: ~ # size variant of image (should be passed to 'type' property) - "~" means "default" - width: 35 - height: 20 - occurrence: 'Front-end: Ordering process' - # additional sizes are used for responsive images in "source" tags in picture element - # "media" should always be provided and contains valid media query - additionalSizes: - - { width: 70, height: 40, media: "only screen and (-webkit-min-device-pixel-ratio: 1.5)" } - - { width: 90, height: 50, media: "only screen and (min-width: 769px) and (-webkit-min-device-pixel-ratio: 1.5)" } - - { width: 45, height: 25, media: "(min-width: 769px)" } -``` - -```tsx -yourComponent.tsx - -import { Image } from 'components/Basic/Image/Image'; -... - -
- {data.name} -
- -... -``` diff --git a/storefront/docs/index.md b/storefront/docs/index.md deleted file mode 100644 index aca502a150..0000000000 --- a/storefront/docs/index.md +++ /dev/null @@ -1,72 +0,0 @@ -This is documentation for Shopsys Platform Storefront. - -## Ways to use Shopsys Storefront - -There are two ways to use Shopsys Storefront on your machine. -First and easiest way is when you have installed your project using Docker. -With Docker, you have everything running already. -If Docker way is too robust for you or you do not need whole application running, you can run Shopsys Storefront natively. - -### Docker way - -With Docker, you have Shopsys Storefront already running. -Storefront is running on http://127.0.0.1:3000 - -#### Restart PNPM - -When you change `next.config.js` file, and you want new settings to be applied, you need to restart PNPM. -You might also want to restart PNPM when something is not working correctly. -In such cases, you do not need to stop all running containers and start them again, just to recreate container of the storefront. -To do that run this command outside the container: - -```plain -docker-compose up -d --force-recreate storefront -``` - -### Native way - -#### Install all dependencies - -```plain -pnpm install -``` - -#### Start app - -```plain -pnpm run dev -``` - -After this command open http://127.0.0.1:3000/ in your browser. - -### Additional commands available for both ways (in Docker way they need to be run inside the storefront container) - -#### Build the app for production. - -```plain -pnpm run build -``` - -#### Run the built app in production mode - -```plain -pnpm start -``` - -#### Run eslint for code - -```plain -pnpm run lint -``` - -#### Run eslint and fix code - -```plain -pnpm run lint--fix -``` - -#### Run prettier format code - -```plain -pnpm run format -``` diff --git a/storefront/docs/redis-graphql-cache.md b/storefront/docs/redis-graphql-cache.md deleted file mode 100644 index f8c5fdfe3d..0000000000 --- a/storefront/docs/redis-graphql-cache.md +++ /dev/null @@ -1,38 +0,0 @@ -## Redis GraphQL cache - -- this cache is used for selected queries and is intended for server side only -- the reason is to improve server side performance of the Storefront - -### How does it work - -- the cache is set via the graphql directive `@_redisCache` which accepts TTL in seconds -- the custom URQl fetcher tries to read the data from the cache, if it does not find it, it calls the API -- the cache can be deactivated (e.g. for development purposes) by setting `GRAPHQL_REDIS_CACHE=0` in your `.env.local` file - -### How to use it - -- to apply cache to some query, simply set the `@_redisCache` directive on the query - -#### Example - -query is not cached -```graphql -query NavigationQuery { - navigation { - name - link - ...CategoriesByColumnFragment - } -} -``` - -query is cached for 1 hour -```graphql -query NavigationQuery @_redisCache(ttl: 3600) { - navigation { - name - link - ...CategoriesByColumnFragment - } -} -``` diff --git a/storefront/docs/robots.txt.md b/storefront/docs/robots.txt.md deleted file mode 100644 index db32cecb03..0000000000 --- a/storefront/docs/robots.txt.md +++ /dev/null @@ -1,7 +0,0 @@ -### Robots.txt - -Robots.txt file is server-side rendered dynamically for each domain, so it's possible to serve sitemap links for the currently visited domain. - -Any individual desired changes in robots.txt file can be achieved in the `pages/robots.txt.tsx` file, function `getRobotsTxtContent` respectively. - -Robots.txt file is available without any necessary additional configurations at the standard location (https://domain/robots.txt) diff --git a/storefront/docs/typescript.md b/storefront/docs/typescript.md deleted file mode 100644 index a7ce336e74..0000000000 --- a/storefront/docs/typescript.md +++ /dev/null @@ -1,146 +0,0 @@ -- this codebase is written in [TypeScript](https://www.typescriptlang.org/) -- the checks are set to strict mode in the tsconfig.json file. -- strict flag enables tighter type checking, which on one hand brings stronger guarantees of code correctness but on the other hand it brings more overhead and requires familiarity with TypeScript development. -- if you are not comfortable with TypeScript, you can set this option to false -- Next.js automatically creates a next-env.d.ts file in the root directory, which cannot be moved, edited or deleted as it can break the application -- you can check the official docs to find out how to use native [React](https://reactjs.org/docs/static-type-checking.html#typescript) or [Next.js](https://nextjs.org/docs/basic-features/typescript) features, such as hooks, SSR, SSG, etc. together with TypeScript -- for other important packages which are used accross this codebase, check the docs below - -#### Creating React components with TypeScript and PropTypes - -**Infering props** - -- when writing components with TypeScript for compile-time checking and PropTypes for run-time checking, you can take advantage of the following type, which you can use to type the props object: - -```plain -function MyComponent( props: InferProps){... -``` - -- this way you allow TypeScript to infer the props from the PropType definitions - -**When the InferProps type is not enough** - -- there will be situations in which this may not be enough (e.g. when passing native onClick events or style object containing CSS properties) -- in cases like these, you can extend the props object with the following TypeScript syntax: - -```plain -function MyComponent( - props: InferProps & { - onClick?: React.MouseEventHandler; - style?: CSSProperties; - children: ReactNode - }, -) -``` - -- generally, every prop you would not explicitly define with PropTypes, you would define like this -- in the code, you can see native props, where we define props in two separate rows. **The first one is for required props** and **the second one is for optional props** - -```plain -type NativeProps = ExtractNativePropsFromDefault< - InputHTMLAttributes, - 'name' | 'id', - 'disabled' | 'style' | 'required' -> -``` - -**Defining PropTypes with TypeScript** - -- for PropTypes and TypeScript to correctly work together, you need to specify all props as required - -```plain -MyComponent.propTypes = { - name: PropTypes.string.isRequired -} -``` - -**Making props optional** - -- then, if you want to make the prop optional, just provide a default value - -```plain -MyComponent.defaultProps = { - name: 'John Doe' -}; -``` - -**Custom enum-like props** - -- sometimes, you will have an enum-like prop (e.g. size prop for button), for which you will have a couple of predefined values (let's say "large" and "small") -- to make this work, you will have to type the oneOf generic function but also provide the values as arguments to the function itself: - -```plain -size: PropTypes.oneOf<'large' | 'small'>(['large', 'small']).isRequired, -``` - -**No implicit default values** - -- when you have a default case in a switch statement or if/else block which are dependent on a prop, you will need to provide an explicit "default" case together with the default prop value - -```plain -... - -switch(props.variant){ - case "default" - Component = - case "primary" - Component = - case "secondary" - Component = -} - -... - -MyComponent.defaultProps = { - variant: 'default' -} - -... - -MyComponent.propTypes{ - variant: PropTypes.oneOf<'default' | 'primary' | 'secondary'>(['default', 'primary', 'secondary']).isRequired -} -``` - -- when using the component, you will not need to provide the prop value if you wish to go with the default case - -```plain - (default) - (primary) - (secondary) -``` - -**Passing props to component** - -- you can easily pass props which have the same name using the spread operator, then specify the rest of the props explicitly - -```plain - -``` - -#### Creating forms with TypeScript and React Hook Form - -- when working with the React Hook Form package, you can use TypeScript to type the hooks and methods provided by the package - -**Typing the useForm hook** - -- you can pass in the types for the form fields using TypeScript types or interfaces - -```plain -type FormFieldsTypes = { - name: string; - age: number; -} - -... - -const formProviderMethods = useForm({...}) -``` - -**Typing the SubmitHandler method** - -- when working with submit handlers, you can specify both the method and the form fields - -```plain -const formSubmitHandler: SubmitHandler = (formFields) => {...} -``` diff --git a/storefront/docs/unit-tests.md b/storefront/docs/unit-tests.md deleted file mode 100644 index 61be45a51f..0000000000 --- a/storefront/docs/unit-tests.md +++ /dev/null @@ -1,364 +0,0 @@ -# Unit tests on storefront - -Unit tests on storefront are written using the [Vitest](https://vitest.dev/) testing library. It has a very similar API to Jest, so anybody with a skill in Vitest or Jest should be able to write them with ease. However, in order to follow a specific guideline and a set of standards, below you can see a _cookbook_ which should help you with writing unit tests for this codebase. - -## Snapshot tests - -To test components and their rendered form, we use snapshot tests. These tests are powerful because of their simplicity and how easy they can discover basic bugs. They are also a great tool for regression testing. However, there are two main things which need to be handled correctly when working with snapshot tests. You can read more about them below. - -### Multiple snapshots per file - -By default, Vitest creates one snapshot per test file. This is sometimes not what you want. In order to change this behavior, you can define your own snapshot by using the `toMatchFileSnapshot` method, for which you can define the name of the snapshot file. This is how it can look in your test suites. - -```tsx -describe('MyComponent snapshot tests', () => { - test('render MyComponent with some props', () => { - const component = render(); - - expect(component).toMatchFileSnapshot('snap-1.test.tsx.snap'); - }); - - test('render MyComponent with other props', () => { - const component = render(); - - expect(component).toMatchFileSnapshot('snap-2.test.tsx.snap'); - }); -``` - -Also keep in mind, that Vitest generates its snapshot in a specific format. For this the files need to have a specific type extension. As you might have noticed in the example above, the file extension is `.test.tsx.snap`. - -### Updating outdated snapshot - -If you change a component which is tested by snapshot tests, these should fail. This is a wanted behavior. However, once you check that your changes are in fact correct, you would want to update these snapshots in order to tell Vitest this is the new correct state of a given component. In order to do that, you can simply run the following command - -```bash -pnpm test--update -``` - -## Config - -Before diving deep into the cookbook for storefront unit tests, there are a couple of interesting vitest config options which should be explained. - -You can read more about the config options in the [Vitest docs](https://vitest.dev/config/). - -```js -export default defineConfig({ - // tsConfigPaths allows us to test our codebase which uses absolute imports based on the TS basePath - plugins: [react(), tsconfigPaths()], - test: { - environment: 'jsdom', - rootDir: './', - // testMatch tells vitest where to search for tests - testMatch: ['vitest/**/*.test.js'], - // the two options below take care of clearing and preparing your mocks for every test - clearMocks: true, - restoreMocks: true, - }, - resolve: { - // these are the directories which are loaded for our tests - // all directories which are included (even indirectly) in our tests should be added here - moduleDirectories: [ - 'node_modules', - 'components', - 'connectors', - 'graphql', - 'helpers', - 'hooks', - 'pages', - 'store', - 'styles', - 'typeHelpers', - 'types', - 'urql', - 'utils', - ], - }, -}); -``` - -## Cookbook - -### The environment of this cookbook - -In this cookbook we will work with a couple of pseudo files, with which some common scenarios will be modelled. In the place of these files, you can put any module or a third party library. The logic should be identical. - -**File _foo.tsx_** - -```tsx -export const getFoo = () => 'foo'; -``` - -**File _bar.tsx_** - -```tsx -import { getFoo } from './foo'; - -export const getBar = () => getFoo(); -``` - -**File _partially-used-file.tsx_** - -```tsx -/** - * This file is only partially used in order to correctly - * show how to mock this type of modules.The purpose of - * this file will be evident later once mocking of partially - * used files or modules will be explained. - */ -export const getFooBar = () => 'foobar'; - -export const EXPORTED_CONSTANT = { - FOO: 'bar', -} as const; - -export const UNUSED_CONSTANT = 'foobar'; -``` - -**File _with-exported-variable.tsx_** - -```tsx -import { EXPORTED_CONSTANT } from 'partially-used-file'; - -export const getExportedVariable = () => EXPORTED_CONSTANT.FOO; -``` - -**File _with-module.tsx_** - -```tsx -/** - * useSessionStore is used because it does not return - * a value directly, but uses a selector. The implementation - * of the useSessionStore function can change in time, but - * for the purpose of this cookbook it is enough if you imagine - * any exported function which needs an anonymous function - * (a selector) to work properly. - */ -import { useSessionStore } from 'store/useSessionStore'; - -export const useModuleValue = () => { - const domainConfig = useSessionStore((s) => s.domainConfig); - - return domainConfig.currencyCode; -}; -``` - -### How to mock different scenarios - -#### 1. Default mock of a function - -This approach is helpful if you want to mock an exported function in a specific way which stays consistent across the file. IF you want this mock function to return the same value for all your test suites inside this file, this is how you do it. - -Later we will see how to modify this default behavior for a specific test. - -```tsx -import { getBar } from './bar'; -import { expect, test, vi } from 'vitest'; - -// default mock of a function -vi.mock('./foo', () => ({ getFoo: vi.fn(() => 'foo default mock') })); - -// test uses default mock, does not need mock override -test('test using default function mock', () => { - // as you can see above, the getBar function uses the getFoo function internally - expect(getBar()).toBe('foo default mock'); -}); -``` - -#### 2. Overriden mock of a function - -If for some reason there are tests which are not well-served by your default function mock, you can override it. - -```tsx -import { getBar } from './bar'; -// the mocked function now needs to be imported -import { getFoo } from './foo'; -import { expect, Mock, test, vi } from 'vitest'; - -// default mock of a function -vi.mock('./foo', () => ({ getFoo: vi.fn(() => 'foo default mock') })); - -// test uses modified behavior of the mock, needs mock override -test('test using overriden function mock', () => { - // type assertion is needed to hack typescript and allow vitest methods - (getFoo as Mock).mockImplementation(() => 'bar'); - expect(getBar()).toBe('bar'); -}); -``` - -#### 3. Default mock of a module - -If you need to mock a module or an external package, you can do it the following way. However, keep in mind that by mocking it like this, you mock the entire behavior of the module. What this means is that if the module exports 3 function and you only mock 1, the other 2 are not available in your tests at all. If this is not what you want, you can check out mocks of a partially mocked modules below. - -```tsx -import { useRouter } from 'next/router'; -import { expect, Mock, test, vi } from 'vitest'; - -// default mock of the next/router module -vi.mock('next/router', () => ({ - // next/router now only contains the useRouter hook - useRouter: vi.fn(() => ({ - // useRouter now only contains these two properties - asPath: '/original', - // your mocks can even have a different interface - push: vi.fn(() => 'mock push'), - })), -})); - -test('test using default module mock', async () => { - // type assertion is needed if the interface of the function changes - expect((useRouter as Mock)().push()).toBe('mock push'); -}); -``` - -#### 4. Overriden mock of a module - -Similar to the examples with exported functions, if you want to override the default mock, you can do it the following way. - -```tsx -import { useRouter } from 'next/router'; -import { expect, Mock, test, vi } from 'vitest'; - -// default mock of the next/router module -vi.mock('next/router', () => ({ - useRouter: vi.fn(() => ({ - asPath: '/original', - push: vi.fn(() => 'mock push'), - })), -})); - -test('test using overriden module mock', () => { - (useRouter as Mock).mockImplementation(() => ({ - asPath: '/overriden', - push: vi.fn(() => 'overriden mock push'), - })); - - expect(useRouter().asPath).toBe('/overriden'); - expect((useRouter as Mock)().push()).toBe('overriden mock push'); -}); -``` - -#### 5. Default mock of a function which uses an anonymous function (selector) - -Some libraries, or even your own code, can export functions which need an anonymous function, a so called selector, to correctly return a value. One of these examples is the useSessionStore hook which we use in the following manner: - -```ts -const foo = useSessionStore((s) => s.foo); -``` - -These functions cannot be mocked as straightforwardly as the functions in the example above. Below you can see an example of a mock which mocks such a function in the correct way. - -```tsx -import { vi } from 'vitest'; - -vi.mock('store/useSessionStore', () => ({ - // selector is used when the mocked function accepts an anonymous function which then returns data - useSessionStore: vi.fn((selector) => { - return selector({ - domainConfig: { - currencyCode: 'USD', - }, - }); - }), -})); -``` - -#### 6. Default mock of a function which uses an anonymous function (selector) - -As in all of the examples above, you can also override a mock of a function which uses a selector. Below you can see an example of how to do so. - -```tsx -import { expect, Mock, test, vi } from 'vitest'; -import { useSessionStore } from 'store/useSessionStore'; - -vi.mock('store/useSessionStore', () => ({ - useSessionStore: vi.fn((selector) => { - return selector({ - domainConfig: { - currencyCode: 'USD', - }, - }); - }), -})); - -test('test using overriden module mock which is called in another file', () => { - (useSessionStore as unknown as Mock).mockImplementation((selector) => { - return selector({ - domainConfig: { - currencyCode: 'CZK', - }, - }); - }); - - expect(useModuleValue()).toBe('CZK'); -}); -``` - -#### 7. Partial mock of a module - -It is often the case that a module exports a wide range of functions and variables. These can then be used in your code and in your tests. If the case is that you only want to mock a part of the module and keep the rest of the code intact, this cannot be done in a naive way. The correct way of partially mocking a module can be seen below. - -```tsx -import { vi } from 'vitest'; - -// by storing this mock in a constant, it can be easily overriden in a specific test -const mockGetFooBar = vi.fn(() => 'default foobar mock'); - -vi.mock('./partially-used-file', async (importOriginal) => { - const actualModuleContents = await importOriginal(); - - return { - // the rest of the module stays in place, only the getFooBar method is mocked - ...actualModuleContents, - getFooBar: mockGetFooBar, - }; -}); -``` - -#### 8. Default mock of an exported variable - -If you want to mock a variable, not a function, you will still have to treat it as a function in a way. Specifically, you will not mock the variable itself, but its getter. This way it can also be overriden in specific tests. However, if you do not care about the possibility of mock override in different tests, you can also do it in a simple way like this: - -```ts -import { vi } from 'vitest'; - -vi.mock('./partially-used-file', () => ({ - EXPORTED_CONSTANT: { - FOO: 'mocked bar', - }, -})); -``` - -But as mentioned above, the more robust way to do it is this: - -```ts -import { vi } from 'vitest'; - -const mockExportedConstantGetter = vi.fn(() => ({ FOO: 'mocked bar' })); -vi.mock('./partially-used-file', () => ({ - get EXPORTED_CONSTANT() { - return mockExportedConstantGetter; - }, -})); -``` - -#### 9. Overriden mock of an exported variable - -With the approach from the previous example, we can easily override a getter of an exported variable for the needs of a specific test. - -```tsx -import { vi } from 'vitest'; -import { getExportedVariable } from './with-exported-variable'; - -const mockExportedConstantGetter = vi.fn(() => ({ FOO: 'mocked bar' })); -vi.mock('./partially-used-file', () => ({ - get EXPORTED_CONSTANT() { - return mockExportedConstantGetter; - }, -})); - -test('test using overriden mock of an exported variable', async () => { - mockExportedConstantGetter.mockImplementation(() => ({ FOO: 'overriden mocked bar' })); - // this function gets the value from the exported variable - expect(getExportedVariable()).toBe('overriden mocked bar'); -}); -```