@akinon/next 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.prettierrc +13 -0
  2. package/api/auth.ts +217 -0
  3. package/api/cache.ts +44 -0
  4. package/api/client.ts +157 -0
  5. package/api/logout.ts +42 -0
  6. package/assets/styles/index.scss +24 -0
  7. package/bin/pz-install-plugins.js +33 -0
  8. package/components/client-root.tsx +69 -0
  9. package/components/image.tsx +133 -0
  10. package/components/mobile-app-toggler.tsx +15 -0
  11. package/components/oauth-login.tsx +24 -0
  12. package/components/plugin-module.tsx +78 -0
  13. package/components/pz-providers.tsx +24 -0
  14. package/components/pz-root.tsx +21 -0
  15. package/components/redirect-three-d/content/index.tsx +64 -0
  16. package/components/redirect-three-d/index.tsx +17 -0
  17. package/components/selected-payment-option-view.tsx +66 -0
  18. package/components/trans.tsx +39 -0
  19. package/data/client/account.ts +188 -0
  20. package/data/client/address.ts +107 -0
  21. package/data/client/api.ts +42 -0
  22. package/data/client/basket.ts +85 -0
  23. package/data/client/checkout.ts +477 -0
  24. package/data/client/misc.ts +101 -0
  25. package/data/client/product.ts +90 -0
  26. package/data/client/user.ts +83 -0
  27. package/data/client/wishlist.ts +79 -0
  28. package/data/server/category.ts +121 -0
  29. package/data/server/flatpage.ts +21 -0
  30. package/data/server/index.ts +8 -0
  31. package/data/server/list.ts +56 -0
  32. package/data/server/menu.ts +35 -0
  33. package/data/server/product.ts +86 -0
  34. package/data/server/seo.ts +48 -0
  35. package/data/server/special-page.ts +42 -0
  36. package/data/server/widget.ts +27 -0
  37. package/data/urls.ts +184 -0
  38. package/hocs/client/index.ts +1 -0
  39. package/hocs/client/with-segment-defaults.tsx +26 -0
  40. package/hocs/server/index.ts +1 -0
  41. package/hocs/server/with-segment-defaults.tsx +83 -0
  42. package/hooks/index.ts +8 -0
  43. package/hooks/use-captcha.tsx +76 -0
  44. package/hooks/use-common-product-attributes.ts +36 -0
  45. package/hooks/use-debounce.ts +20 -0
  46. package/hooks/use-localization.ts +63 -0
  47. package/hooks/use-media-query.ts +36 -0
  48. package/hooks/use-on-click-outside.tsx +28 -0
  49. package/hooks/use-router.ts +45 -0
  50. package/hooks/use-translation.ts +14 -0
  51. package/lib/cache.ts +185 -0
  52. package/localization/index.ts +5 -0
  53. package/localization/provider.tsx +58 -0
  54. package/middlewares/currency.ts +55 -0
  55. package/middlewares/default.ts +224 -0
  56. package/middlewares/index.ts +29 -0
  57. package/middlewares/locale.ts +61 -0
  58. package/middlewares/oauth-login.ts +78 -0
  59. package/middlewares/pretty-url.ts +94 -0
  60. package/middlewares/redirection-payment.ts +117 -0
  61. package/middlewares/three-d-redirection.ts +122 -0
  62. package/middlewares/url-redirection.ts +61 -0
  63. package/package.json +20 -0
  64. package/plugins.js +7 -0
  65. package/redux/hooks.ts +7 -0
  66. package/redux/middlewares/checkout.ts +231 -0
  67. package/redux/middlewares/index.ts +50 -0
  68. package/redux/reducers/checkout.ts +164 -0
  69. package/redux/reducers/config.ts +28 -0
  70. package/redux/reducers/header.ts +59 -0
  71. package/redux/reducers/index.ts +15 -0
  72. package/redux/reducers/root.ts +61 -0
  73. package/tailwind/rtl.js +137 -0
  74. package/types/commerce/account.ts +68 -0
  75. package/types/commerce/address.ts +94 -0
  76. package/types/commerce/basket.ts +43 -0
  77. package/types/commerce/category.ts +114 -0
  78. package/types/commerce/checkout.ts +132 -0
  79. package/types/commerce/flatpage.ts +7 -0
  80. package/types/commerce/index.ts +10 -0
  81. package/types/commerce/misc.ts +127 -0
  82. package/types/commerce/order.ts +108 -0
  83. package/types/commerce/product.ts +110 -0
  84. package/types/commerce/widget.ts +28 -0
  85. package/types/gtm.ts +16 -0
  86. package/types/index.ts +207 -0
  87. package/types/next-auth.d.ts +24 -0
  88. package/utils/app-fetch.ts +62 -0
  89. package/utils/generate-commerce-search-params.ts +22 -0
  90. package/utils/get-currency.ts +29 -0
  91. package/utils/image-loader.ts +31 -0
  92. package/utils/index.ts +132 -0
  93. package/utils/localization.ts +29 -0
  94. package/utils/log.ts +138 -0
  95. package/utils/menu-generator.ts +27 -0
  96. package/utils/mobile-3d-iframe.ts +58 -0
  97. package/utils/server-translation.ts +57 -0
  98. package/utils/server-variables.ts +9 -0
@@ -0,0 +1,48 @@
1
+ import appFetch from '../../utils/app-fetch';
2
+ import { Cache, CacheKey } from '../../lib/cache';
3
+ import { misc } from '../../data/urls';
4
+ import logger from '../../utils/log';
5
+
6
+ function getRootSeoDataHandler() {
7
+ return async function () {
8
+ let data = {};
9
+
10
+ try {
11
+ data = await appFetch<{ [key: string]: string }>(misc.cmsSeo('/'));
12
+ } catch (error) {
13
+ logger.error('Error while fetching root seo data', error);
14
+ }
15
+
16
+ return data;
17
+ };
18
+ }
19
+
20
+ function getSeoDataHandler(url: string) {
21
+ return async function () {
22
+ let data = {} as {
23
+ title: string;
24
+ description: string;
25
+ keywords: string;
26
+ [key: string]: string;
27
+ };
28
+
29
+ try {
30
+ data = await appFetch(misc.cmsSeo(url));
31
+ } catch (error) {
32
+ logger.error('Error while fetching seo data', { url, error });
33
+ }
34
+
35
+ return data;
36
+ };
37
+ }
38
+
39
+ /**
40
+ * @deprecated Use getSeoData instead
41
+ */
42
+ export const getRootSeo = async () => {
43
+ return Cache.wrap(CacheKey.RootSeo, getRootSeoDataHandler());
44
+ };
45
+
46
+ export const getSeoData = async (url: string) => {
47
+ return Cache.wrap(CacheKey.Seo(url), getSeoDataHandler(url));
48
+ };
@@ -0,0 +1,42 @@
1
+ import { Cache, CacheKey } from '../../lib/cache';
2
+ import { category } from '../urls';
3
+ import { GetCategoryResponse } from '../../types';
4
+ import { generateCommerceSearchParams } from '../../utils';
5
+ import appFetch from '../../utils/app-fetch';
6
+
7
+ const getSpecialPageDataHandler = (
8
+ pk: number,
9
+ searchParams: URLSearchParams
10
+ ) => {
11
+ return async function () {
12
+ const params = generateCommerceSearchParams(searchParams);
13
+
14
+ const data: GetCategoryResponse = await appFetch(
15
+ `${category.getSpecialPageByPk(pk)}${params}`,
16
+ {
17
+ headers: {
18
+ Accept: 'application/json',
19
+ 'Content-Type': 'application/json'
20
+ }
21
+ }
22
+ );
23
+
24
+ return data;
25
+ };
26
+ };
27
+
28
+ export const getSpecialPageData = async ({
29
+ pk,
30
+ searchParams
31
+ }: {
32
+ pk: number;
33
+ searchParams: URLSearchParams;
34
+ }) => {
35
+ return Cache.wrap(
36
+ CacheKey.SpecialPage(pk, searchParams),
37
+ getSpecialPageDataHandler(pk, searchParams),
38
+ {
39
+ expire: 300
40
+ }
41
+ );
42
+ };
@@ -0,0 +1,27 @@
1
+ import { Cache, CacheKey } from '../../lib/cache';
2
+ import 'server-only';
3
+ import { CacheOptions, WidgetResultType } from '../../types';
4
+ import appFetch from '../../utils/app-fetch';
5
+ import { widgets } from '../urls';
6
+
7
+ const getWidgetDataHandler = (slug: string) => async () => {
8
+ if (!slug) {
9
+ return null;
10
+ }
11
+
12
+ return await appFetch(widgets.getWidgets(slug));
13
+ };
14
+
15
+ export const getWidgetData = async <T>({
16
+ slug,
17
+ cacheOptions
18
+ }: {
19
+ slug: string;
20
+ cacheOptions?: CacheOptions;
21
+ }): Promise<WidgetResultType<T>> => {
22
+ return Cache.wrap(
23
+ CacheKey.Widget(slug),
24
+ getWidgetDataHandler(slug),
25
+ cacheOptions
26
+ );
27
+ };
package/data/urls.ts ADDED
@@ -0,0 +1,184 @@
1
+ import Settings from 'settings';
2
+
3
+ const API_URL = Settings.commerceUrl;
4
+
5
+ export const account = {
6
+ order: '/users/orders',
7
+ orderId: (id: string | string[]) => `/users/orders/${id}`,
8
+ cancelOrder: (id: string) => `/orders/${id}/cancel`,
9
+ cancellationReasons: '/users/orders/cancellation_reasons',
10
+ updatePassword: '/users/password/change',
11
+ updateEmail: '/users/email-change',
12
+ updateProfile: '/users/profile',
13
+ getContactSubjects: '/users/contact-us-subjects',
14
+ getOrders: ({
15
+ page,
16
+ limit,
17
+ createdDate
18
+ }: {
19
+ page?: number;
20
+ limit?: number;
21
+ createdDate?: string;
22
+ }) =>
23
+ `/users/orders?page=${page || 1}&limit=${limit || 12} ${
24
+ createdDate ? `&created_date__gte=${createdDate}` : ''
25
+ }`,
26
+ sendContact: '/users/contact-us',
27
+ passwordReset: (slug: string) => `/users/reset/${slug}`,
28
+ anonymize: '/users/anonymize/',
29
+ loyaltyBalance: '/account/loyalty-account-balance/',
30
+ loyaltyTransactions: '/account/loyalty/'
31
+ };
32
+
33
+ export const address = {
34
+ base: '/address',
35
+ countries: '/address/country',
36
+ getAddresses: '/address/detailed',
37
+ getCities: (country: string) => `/address/city?country=${country}`,
38
+ getTownships: (city: string) => `/address/township?city=${city}`,
39
+ getDistricts: (township: string) => `/address/district?township=${township}`,
40
+ getRetailStore: '/address/city?retailstore__isnull=false',
41
+ getRetailStoreCities: (country: string) =>
42
+ `/address/city?retailstore__isnull=false&country=${country}`,
43
+ getRetailStoreTownships: (city: string) =>
44
+ `/address/township?retailstore__isnull=false&city=${city}`,
45
+ editAddress: (pk: number) => `/address/${pk}`,
46
+ removeAddress: (id: number) => `/address/${id}`,
47
+ setDefaultAddress: (pk: number) => `/address/${pk}`
48
+ };
49
+
50
+ export const basket = {
51
+ getBasket: '/baskets/basket'
52
+ };
53
+
54
+ export const category = {
55
+ // eslint-disable-next-line projectzero/client-url
56
+ list: '/list/',
57
+ getCategoryByPk: (pk: number) => `/category/${pk}/`,
58
+ getCategoryBySlug: (slug: string) => `/${slug}`,
59
+ getSpecialPageByPk: (pk: number) => `/special-page/${pk}/`
60
+ };
61
+
62
+ export const checkout = {
63
+ get3dRedirectForm: '/orders/redirect-three-d',
64
+ fetchCheckout: '/orders/checkout',
65
+ fetchCheckoutResult: (token: string) =>
66
+ `/orders/completed/${token}?format=json`,
67
+ getContract: (slug: string) => `/orders/contract?contract_name=${slug}`,
68
+ completeCreditCardPayment: '/orders/checkout?page=CreditCardConfirmationPage',
69
+ completeFundsTransfer: '/orders/checkout?page=FundsTransferPage',
70
+ guestLogin: '/orders/checkout?page=IndexPage',
71
+ setDeliveryOption: '/orders/checkout?page=DeliveryOptionSelectionPage',
72
+ setAddresses: '/orders/checkout?page=AddressSelectionPage',
73
+ setShippingOption: '/orders/checkout?page=ShippingOptionSelectionPage',
74
+ setPaymentOption: '/orders/checkout?page=PaymentOptionSelectionPage',
75
+ setBinNumber: '/orders/checkout?page=BinNumberPage',
76
+ setMasterpassBinNumber: '/orders/checkout?page=MasterpassBinNumberPage',
77
+ setInstallmentOption: '/orders/checkout?page=InstallmentSelectionPage',
78
+ setMasterPassInstallmentOption:
79
+ '/orders/checkout?page=MasterpassInstallmentPage',
80
+ setFundsTransferOption: '/orders/checkout?page=FundsTransferChoicePage',
81
+ getCheckoutProviders: '/orders/checkout?page=CheckoutProviderIndexPage',
82
+ setCheckoutProvider: '/orders/checkout?page=CheckoutProviderSelectionPage',
83
+ completeRedirectionPayment:
84
+ '/orders/checkout?page=RedirectionPaymentSelectedPage',
85
+ confirmationPaymentSelect:
86
+ '/orders/checkout?page=ConfirmationPaymentSelectedPage',
87
+ confirmationQuery: '/orders/checkout?page=ConfirmationPaymentQueryPage',
88
+ confirmationComplete: '/orders/checkout?page=ConfirmationPaymentCompletePage',
89
+ completeLoyaltyPayment: '/orders/checkout?page=LoyaltyPaymentSelectedPage',
90
+ loyaltyMoneyUsage: '/orders/checkout?page=LoyaltyMoneyUsagePage',
91
+ setOrderNote: '/orders/checkout?page=OrderNotePage'
92
+ };
93
+
94
+ export const flatpage = {
95
+ getFlatPageByPk: (pk: number) => `/shop-flat-page/${pk}/`
96
+ };
97
+
98
+ export const misc = {
99
+ autocomplete: '/autocomplete',
100
+ stores: '/stores',
101
+ menu: '/misc/menus/generate',
102
+ prettyUrl: (pathname: string) => `/misc/pretty-url${pathname}/`,
103
+ prettyUrls: (pathname: string) => `/pretty_urls/?new_path__exact=${pathname}`,
104
+ emailSubscription: '/email-subscription',
105
+ menus: (depthHeight: number, parent?: string) =>
106
+ `/menus/generate/?depth_height=${depthHeight}${
107
+ parent ? `&parent=${parent}` : ''
108
+ }`,
109
+ // eslint-disable-next-line projectzero/client-url
110
+ cmsSeo: (slug: string | string[]) => `/cms/seo/?url=${slug ? slug : '/'}`,
111
+ setCurrency: '/users/activate-currency'
112
+ };
113
+
114
+ export const product = {
115
+ category: (productId: number) => `/product/${productId}/category/`,
116
+ installments: (productId: number) => `/payments/cards/product/${productId}`,
117
+ getProductByPk: (pk: number) => `/product/${pk}`,
118
+ getGroupProductByPk: (pk: number) => `/group-product/${pk}`,
119
+ getRetailStoreStock: (productPk: string, store: string, hasVariant: string) =>
120
+ `/retail_store_stock/${productPk}?city_id=${store}${hasVariant}`,
121
+ addProduct: '/baskets/basket',
122
+ slug: (slug: string) => `/${slug}`,
123
+ categoryUrl: (pk: number) => `/products/${pk}/category_nodes/?limit=1`,
124
+ breadcrumbUrl: (menuitemmodel: string) =>
125
+ `/menus/generate_breadcrumb/?item=${menuitemmodel}&generator_name=menu_item`
126
+ };
127
+
128
+ export const wishlist = {
129
+ getFavorites: ({ page, limit }: { page?: number; limit?: number }) =>
130
+ `/wishlists/favourite-products?limit=${limit || 12}&page=${page || 1}`,
131
+ addFavorite: '/wishlists/favourite-products',
132
+ removeFavorite: (favPk: number) => `/wishlists/favourite-products/${favPk}`,
133
+ addStockAlert: '/wishlists/product-alerts'
134
+ };
135
+
136
+ export const user = {
137
+ currentUser: '/current_user',
138
+ login: '/users/login',
139
+ // eslint-disable-next-line projectzero/client-url
140
+ register: '/users/registration/',
141
+ logout: '/users/logout',
142
+ captcha: '/users/pz-captcha',
143
+ profiles: '/users/profile',
144
+ forgotPassword: '/users/password/reset',
145
+ csrfToken: '/csrf_token'
146
+ };
147
+
148
+ export const widgets = {
149
+ getWidgets: (slug: string) => `/widgets/${slug}`
150
+ };
151
+
152
+ const URLS = {
153
+ account,
154
+ address,
155
+ basket,
156
+ category,
157
+ checkout,
158
+ flatpage,
159
+ misc,
160
+ product,
161
+ wishlist,
162
+ user,
163
+ widgets
164
+ };
165
+
166
+ const UrlProxyHandler = {
167
+ get(target, prop, receiver) {
168
+ if (typeof target[prop] === 'function') {
169
+ return new Proxy(target[prop], {
170
+ apply(target, thisArgs, argumentList) {
171
+ return `${API_URL}${target(...argumentList)}`;
172
+ }
173
+ });
174
+ }
175
+
176
+ return `${API_URL}${target[prop]}`;
177
+ }
178
+ };
179
+
180
+ Object.keys(URLS).forEach((key) => {
181
+ URLS[key] = new Proxy(URLS[key], UrlProxyHandler);
182
+ });
183
+
184
+ export { URLS };
@@ -0,0 +1 @@
1
+ export * from './with-segment-defaults';
@@ -0,0 +1,26 @@
1
+ import { LayoutProps, PageProps, RootLayoutProps } from '../../types';
2
+ import React from 'react';
3
+
4
+ type SegmentType = 'client-root' | 'layout' | 'page';
5
+
6
+ interface SegmentDefaultsOptions {
7
+ segmentType: SegmentType;
8
+ }
9
+
10
+ export const withSegmentDefaults =
11
+ <T extends PageProps | LayoutProps | RootLayoutProps>(
12
+ Component: (
13
+ props?: T
14
+ ) => null | JSX.Element | Promise<JSX.Element> | Promise<JSX.Element[]>,
15
+ options: SegmentDefaultsOptions
16
+ ) =>
17
+ (props: T) => {
18
+ let componentProps = { ...props };
19
+
20
+ return (
21
+ <>
22
+ {/* @ts-expect-error Server Component */}
23
+ <Component {...componentProps} />
24
+ </>
25
+ );
26
+ };
@@ -0,0 +1 @@
1
+ export * from './with-segment-defaults';
@@ -0,0 +1,83 @@
1
+ import settings from 'settings';
2
+ import { LayoutProps, PageProps, RootLayoutProps } from '../../types';
3
+ import { redirect } from 'next/navigation';
4
+ import { ServerVariables } from '../../utils/server-variables';
5
+ import { getTranslations } from '../../utils/server-translation';
6
+ import { ROUTES } from 'routes';
7
+ import logger from '../../utils/log';
8
+
9
+ type SegmentType = 'root-layout' | 'layout' | 'page';
10
+
11
+ interface SegmentDefaultsOptions {
12
+ segmentType: SegmentType;
13
+ }
14
+
15
+ export const withSegmentDefaults =
16
+ <T extends PageProps | LayoutProps | RootLayoutProps>(
17
+ Component: (
18
+ props?: T
19
+ ) => null | JSX.Element | Promise<JSX.Element> | Promise<JSX.Element[]>,
20
+ options: SegmentDefaultsOptions
21
+ ) =>
22
+ async (props: T) => {
23
+ let componentProps = { ...props };
24
+
25
+ if (options.segmentType === 'root-layout') {
26
+ componentProps = (await addRootLayoutProps(
27
+ componentProps as RootLayoutProps
28
+ )) as T;
29
+
30
+ checkRedisVariables();
31
+ }
32
+
33
+ ServerVariables.locale = props.params.locale;
34
+ ServerVariables.currency = props.params.currency;
35
+
36
+ return await (
37
+ <>
38
+ {/* @ts-expect-error Server Component */}
39
+ <Component {...componentProps} />
40
+ </>
41
+ );
42
+ };
43
+
44
+ const addRootLayoutProps = async (componentProps: RootLayoutProps) => {
45
+ const params = componentProps.params;
46
+
47
+ if (
48
+ params.commerce !== encodeURIComponent(decodeURI(settings.commerceUrl)) ||
49
+ !settings.localization.locales.find((l) => l.value === params.locale)
50
+ ) {
51
+ return redirect(ROUTES.HOME);
52
+ }
53
+
54
+ const translations = await getTranslations(params.locale);
55
+ componentProps.translations = translations;
56
+
57
+ const locale = settings.localization.locales.find(
58
+ (l) => l.value === params.locale
59
+ );
60
+ const [isoCode] = locale.value.split('-');
61
+
62
+ componentProps.locale = {
63
+ ...locale,
64
+ isoCode
65
+ };
66
+
67
+ return componentProps;
68
+ };
69
+
70
+ const checkRedisVariables = () => {
71
+ const requiredVariableValues = [
72
+ process.env.CACHE_HOST,
73
+ process.env.CACHE_PORT,
74
+ process.env.CACHE_SECRET
75
+ ];
76
+
77
+ if (!requiredVariableValues.every((v) => v)) {
78
+ logger.warn('Redis cache variables are not set', {
79
+ requiredVariables: ['CACHE_HOST', 'CACHE_PORT', 'CACHE_SECRET'],
80
+ values: requiredVariableValues
81
+ });
82
+ }
83
+ };
package/hooks/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './use-router';
2
+ export * from './use-localization';
3
+ export * from './use-translation';
4
+ export * from './use-captcha';
5
+ export * from './use-common-product-attributes';
6
+ export * from './use-debounce';
7
+ export * from './use-media-query';
8
+ export * from './use-on-click-outside';
@@ -0,0 +1,76 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import ReCAPTCHA from 'react-google-recaptcha';
5
+ import {
6
+ useGetCaptchaQuery,
7
+ useValidateCaptchaMutation
8
+ } from '../data/client/user';
9
+ import { setCookie } from '../utils';
10
+
11
+ interface CaptchaViewProps {
12
+ className?: string;
13
+ }
14
+
15
+ export const useCaptcha = () => {
16
+ const { data } = useGetCaptchaQuery();
17
+ const { siteKey, csrfToken } = data ?? {};
18
+ const [validateCaptchaMutation] = useValidateCaptchaMutation();
19
+ const [validated, setValidated] = useState<boolean>(false);
20
+ const [isVisible, setIsVisible] = useState<boolean>(false);
21
+ const [captchaResponse, setCaptchaResponse] = useState<string>(null);
22
+
23
+ const validate = async () => {
24
+ if (!captchaResponse) {
25
+ setIsVisible(true);
26
+ return false;
27
+ }
28
+
29
+ setValidated(false);
30
+
31
+ const response = await validateCaptchaMutation({
32
+ captchaResponse,
33
+ csrfToken
34
+ }).unwrap();
35
+
36
+ setValidated(response.success);
37
+
38
+ return response.success;
39
+ };
40
+
41
+ if (csrfToken) {
42
+ setCookie('csrftoken', csrfToken);
43
+ }
44
+
45
+ const onCaptchaChange = useCallback(async (response) => {
46
+ setTimeout(() => {
47
+ setCaptchaResponse(response);
48
+ }, 500);
49
+ }, []);
50
+
51
+ useEffect(() => {
52
+ if (captchaResponse) {
53
+ validate();
54
+ }
55
+ }, [captchaResponse]);
56
+
57
+ const CaptchaView = useMemo(() => {
58
+ const View = (props: CaptchaViewProps) =>
59
+ isVisible && !captchaResponse ? (
60
+ <ReCAPTCHA
61
+ sitekey={siteKey}
62
+ onChange={onCaptchaChange}
63
+ className={props.className}
64
+ />
65
+ ) : null;
66
+
67
+ return View;
68
+ }, [siteKey, onCaptchaChange, isVisible, captchaResponse]);
69
+
70
+ return {
71
+ CaptchaView,
72
+ validated,
73
+ isVisible,
74
+ validate
75
+ };
76
+ };
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import Settings from 'settings';
4
+ import { useLocalization } from './use-localization';
5
+
6
+ type UseCommonProductAttributesArgs = {
7
+ attributes: { [key: string]: { value: string } };
8
+ };
9
+
10
+ type CommonProductAttributes = { name: string; value: string }[];
11
+
12
+ export const useCommonProductAttributes = (
13
+ args: UseCommonProductAttributesArgs
14
+ ) => {
15
+ const { attributes } = args;
16
+ const commonProductAttributes: CommonProductAttributes = [];
17
+ const { t } = useLocalization();
18
+
19
+ Settings.commonProductAttributes.forEach((commonProductAttribute) => {
20
+ const attribute = Object.entries(attributes).find(
21
+ // Object.entries gives us an array of [key, value] pairs.
22
+ ([key]) => key === commonProductAttribute.key
23
+ )?.[1]; // We only want the value, so we get the second item in the array.
24
+
25
+ if (attribute) {
26
+ commonProductAttributes.push({
27
+ name: t(
28
+ `common.common_product_attributes.${commonProductAttribute.translationKey}`
29
+ ),
30
+ value: attribute.value
31
+ });
32
+ }
33
+ });
34
+
35
+ return commonProductAttributes;
36
+ };
@@ -0,0 +1,20 @@
1
+ // https://usehooks.com/useDebounce/
2
+ 'use client';
3
+
4
+ import { useEffect, useState } from 'react';
5
+
6
+ export function useDebounce<T>(value: T, delay: number): T {
7
+ const [debouncedValue, setDebouncedValue] = useState<T>(value);
8
+
9
+ useEffect(() => {
10
+ const handler = setTimeout(() => {
11
+ setDebouncedValue(value);
12
+ }, delay);
13
+
14
+ return () => {
15
+ clearTimeout(handler);
16
+ };
17
+ }, [value, delay]);
18
+
19
+ return debouncedValue;
20
+ }
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import { LocalizationContext } from '../localization/provider';
4
+ import { useContext } from 'react';
5
+ import { urlLocaleMatcherRegex } from '../utils';
6
+ import { LocaleUrlStrategy } from '../localization';
7
+ import { useRouter } from 'next/navigation';
8
+
9
+ export const useLocalization = () => {
10
+ const {
11
+ translate,
12
+ locale,
13
+ currency,
14
+ locales,
15
+ currencies,
16
+ defaultLocaleValue,
17
+ defaultCurrencyCode,
18
+ localeUrlStrategy
19
+ } = useContext(LocalizationContext);
20
+
21
+ const router = useRouter();
22
+
23
+ /**
24
+ * Sets the locale in the URL.
25
+ * @param locale Locale value defined in the settings.
26
+ */
27
+ const setLocale = (locale: string) => {
28
+ const localePath =
29
+ locale === defaultLocaleValue &&
30
+ localeUrlStrategy !== LocaleUrlStrategy.ShowAllLocales
31
+ ? ''
32
+ : `/${locale}`;
33
+
34
+ const pathnameWithoutLocale = location.pathname.replace(
35
+ urlLocaleMatcherRegex,
36
+ ''
37
+ );
38
+
39
+ router.push(`${localePath}${pathnameWithoutLocale}${location.search}`);
40
+ };
41
+
42
+ return {
43
+ /**
44
+ * Translate function
45
+ * @param path
46
+ * Path of the translation.
47
+ * It is a dot separated string e.g. "common.header.title".
48
+ * "common" is the file name in the translations folder.
49
+ * "header" is the key in the file.
50
+ * "title" is the value of the key.
51
+ * @returns {string} Translated value
52
+ */
53
+ t: (path: string) => translate(path),
54
+ setLocale,
55
+ locale,
56
+ currency,
57
+ locales,
58
+ currencies,
59
+ defaultLocaleValue,
60
+ defaultCurrencyCode,
61
+ localeUrlStrategy,
62
+ };
63
+ };
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ export function useMediaQuery(query: string): boolean {
6
+ const getMatches = (query: string): boolean => {
7
+ if (typeof window !== 'undefined') {
8
+ return window.matchMedia(query).matches;
9
+ }
10
+ return false;
11
+ };
12
+
13
+ const [matches, setMatches] = useState<boolean>(getMatches(query));
14
+
15
+ const handleChange = () => {
16
+ setMatches(getMatches(query));
17
+ };
18
+
19
+ useEffect(() => {
20
+ const matchMedia = window.matchMedia(query);
21
+
22
+ handleChange();
23
+
24
+ if (matchMedia.addEventListener) {
25
+ matchMedia.addEventListener('change', handleChange);
26
+ }
27
+
28
+ return () => {
29
+ if (matchMedia.removeEventListener) {
30
+ matchMedia.removeEventListener('change', handleChange);
31
+ }
32
+ };
33
+ }, []);
34
+
35
+ return matches;
36
+ }
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+
3
+ import { useEffect, RefObject } from 'react';
4
+
5
+ type Event = MouseEvent | TouchEvent;
6
+
7
+ export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
8
+ ref: RefObject<T>,
9
+ callback: (event: Event) => void,
10
+ mouseEvent: 'mousedown' | 'mouseup' | 'click' = 'click'
11
+ ): void {
12
+ useEffect(() => {
13
+ const onClickHandler = (event: Event) => {
14
+ const el = ref?.current;
15
+
16
+ if (!el || el.contains((event?.target as Node) || null)) {
17
+ return;
18
+ }
19
+
20
+ callback(event);
21
+ };
22
+
23
+ document.addEventListener(mouseEvent, onClickHandler);
24
+ return () => {
25
+ document.removeEventListener(mouseEvent, onClickHandler);
26
+ };
27
+ }, [callback, mouseEvent, ref]);
28
+ }