@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.
- package/.prettierrc +13 -0
- package/api/auth.ts +217 -0
- package/api/cache.ts +44 -0
- package/api/client.ts +157 -0
- package/api/logout.ts +42 -0
- package/assets/styles/index.scss +24 -0
- package/bin/pz-install-plugins.js +33 -0
- package/components/client-root.tsx +69 -0
- package/components/image.tsx +133 -0
- package/components/mobile-app-toggler.tsx +15 -0
- package/components/oauth-login.tsx +24 -0
- package/components/plugin-module.tsx +78 -0
- package/components/pz-providers.tsx +24 -0
- package/components/pz-root.tsx +21 -0
- package/components/redirect-three-d/content/index.tsx +64 -0
- package/components/redirect-three-d/index.tsx +17 -0
- package/components/selected-payment-option-view.tsx +66 -0
- package/components/trans.tsx +39 -0
- package/data/client/account.ts +188 -0
- package/data/client/address.ts +107 -0
- package/data/client/api.ts +42 -0
- package/data/client/basket.ts +85 -0
- package/data/client/checkout.ts +477 -0
- package/data/client/misc.ts +101 -0
- package/data/client/product.ts +90 -0
- package/data/client/user.ts +83 -0
- package/data/client/wishlist.ts +79 -0
- package/data/server/category.ts +121 -0
- package/data/server/flatpage.ts +21 -0
- package/data/server/index.ts +8 -0
- package/data/server/list.ts +56 -0
- package/data/server/menu.ts +35 -0
- package/data/server/product.ts +86 -0
- package/data/server/seo.ts +48 -0
- package/data/server/special-page.ts +42 -0
- package/data/server/widget.ts +27 -0
- package/data/urls.ts +184 -0
- package/hocs/client/index.ts +1 -0
- package/hocs/client/with-segment-defaults.tsx +26 -0
- package/hocs/server/index.ts +1 -0
- package/hocs/server/with-segment-defaults.tsx +83 -0
- package/hooks/index.ts +8 -0
- package/hooks/use-captcha.tsx +76 -0
- package/hooks/use-common-product-attributes.ts +36 -0
- package/hooks/use-debounce.ts +20 -0
- package/hooks/use-localization.ts +63 -0
- package/hooks/use-media-query.ts +36 -0
- package/hooks/use-on-click-outside.tsx +28 -0
- package/hooks/use-router.ts +45 -0
- package/hooks/use-translation.ts +14 -0
- package/lib/cache.ts +185 -0
- package/localization/index.ts +5 -0
- package/localization/provider.tsx +58 -0
- package/middlewares/currency.ts +55 -0
- package/middlewares/default.ts +224 -0
- package/middlewares/index.ts +29 -0
- package/middlewares/locale.ts +61 -0
- package/middlewares/oauth-login.ts +78 -0
- package/middlewares/pretty-url.ts +94 -0
- package/middlewares/redirection-payment.ts +117 -0
- package/middlewares/three-d-redirection.ts +122 -0
- package/middlewares/url-redirection.ts +61 -0
- package/package.json +20 -0
- package/plugins.js +7 -0
- package/redux/hooks.ts +7 -0
- package/redux/middlewares/checkout.ts +231 -0
- package/redux/middlewares/index.ts +50 -0
- package/redux/reducers/checkout.ts +164 -0
- package/redux/reducers/config.ts +28 -0
- package/redux/reducers/header.ts +59 -0
- package/redux/reducers/index.ts +15 -0
- package/redux/reducers/root.ts +61 -0
- package/tailwind/rtl.js +137 -0
- package/types/commerce/account.ts +68 -0
- package/types/commerce/address.ts +94 -0
- package/types/commerce/basket.ts +43 -0
- package/types/commerce/category.ts +114 -0
- package/types/commerce/checkout.ts +132 -0
- package/types/commerce/flatpage.ts +7 -0
- package/types/commerce/index.ts +10 -0
- package/types/commerce/misc.ts +127 -0
- package/types/commerce/order.ts +108 -0
- package/types/commerce/product.ts +110 -0
- package/types/commerce/widget.ts +28 -0
- package/types/gtm.ts +16 -0
- package/types/index.ts +207 -0
- package/types/next-auth.d.ts +24 -0
- package/utils/app-fetch.ts +62 -0
- package/utils/generate-commerce-search-params.ts +22 -0
- package/utils/get-currency.ts +29 -0
- package/utils/image-loader.ts +31 -0
- package/utils/index.ts +132 -0
- package/utils/localization.ts +29 -0
- package/utils/log.ts +138 -0
- package/utils/menu-generator.ts +27 -0
- package/utils/mobile-3d-iframe.ts +58 -0
- package/utils/server-translation.ts +57 -0
- 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
|
+
}
|