@magento/peregrine 14.4.1 → 14.5.1-alpha.4
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/lib/store/actions/user/actions.js +6 -1
- package/lib/store/actions/user/asyncActions.js +6 -0
- package/lib/store/reducers/user.js +8 -1
- package/lib/talons/CheckoutPage/ItemsReview/__fixtures__/cartItems.js +121 -94
- package/lib/talons/CheckoutPage/ItemsReview/useItemsReview.js +10 -30
- package/lib/talons/CheckoutPage/OrderConfirmationPage/orderConfirmationPage.gql.js +30 -0
- package/lib/talons/CheckoutPage/OrderConfirmationPage/useCreateAccount.js +6 -1
- package/lib/talons/CheckoutPage/OrderConfirmationPage/useOrderConfirmationPage.js +77 -5
- package/lib/talons/CheckoutPage/useCheckoutPage.js +14 -1
- package/lib/talons/CreateAccount/createAccount.gql.js +1 -0
- package/lib/talons/CreateAccount/useCreateAccount.js +21 -2
- package/lib/talons/FilterModal/helpers.js +1 -1
- package/lib/talons/FilterModal/useFilterBlock.js +9 -2
- package/lib/talons/FilterSidebar/useFilterSidebar.js +4 -3
- package/lib/talons/FormError/useFormError.js +17 -1
- package/lib/talons/OrderHistoryPage/orderRow.gql.js +1 -1
- package/lib/talons/RootComponents/Category/categoryContent.gql.js +2 -4
- package/lib/talons/RootComponents/Category/useCategoryContent.js +105 -9
- package/lib/talons/SignIn/useSignIn.js +20 -4
- package/lib/util/htmlStringImgUrlConverter.js +2 -2
- package/lib/util/images.js +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { createActions } from 'redux-actions';
|
|
2
2
|
|
|
3
3
|
const prefix = 'USER';
|
|
4
|
-
const actionTypes = [
|
|
4
|
+
const actionTypes = [
|
|
5
|
+
'RESET',
|
|
6
|
+
'SET_TOKEN',
|
|
7
|
+
'CLEAR_TOKEN',
|
|
8
|
+
'SET_USER_ON_ORDER_SUCCESS'
|
|
9
|
+
];
|
|
5
10
|
|
|
6
11
|
const actionMap = {
|
|
7
12
|
SIGN_IN: {
|
|
@@ -89,3 +89,9 @@ export const clearToken = () =>
|
|
|
89
89
|
// Remove from store
|
|
90
90
|
dispatch(actions.clearToken());
|
|
91
91
|
};
|
|
92
|
+
|
|
93
|
+
export const setUserOnOrderSuccess = successFlag =>
|
|
94
|
+
async function thunk(dispatch) {
|
|
95
|
+
// Dispatch the action to update the state
|
|
96
|
+
dispatch(actions.setUserOnOrderSuccess(successFlag));
|
|
97
|
+
};
|
|
@@ -30,7 +30,8 @@ const initialState = {
|
|
|
30
30
|
isResettingPassword: false,
|
|
31
31
|
isSignedIn: isSignedIn(),
|
|
32
32
|
resetPasswordError: null,
|
|
33
|
-
token: getToken()
|
|
33
|
+
token: getToken(),
|
|
34
|
+
userOnOrderSuccess: false // Add userOnOrderSuccess state
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
const reducerMap = {
|
|
@@ -48,6 +49,12 @@ const reducerMap = {
|
|
|
48
49
|
token: null
|
|
49
50
|
};
|
|
50
51
|
},
|
|
52
|
+
[actions.setUserOnOrderSuccess]: (state, { payload }) => {
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
userOnOrderSuccess: payload // Update the state with the new flag value
|
|
56
|
+
};
|
|
57
|
+
},
|
|
51
58
|
[actions.getDetails.request]: state => {
|
|
52
59
|
return {
|
|
53
60
|
...state,
|
|
@@ -1,102 +1,129 @@
|
|
|
1
|
-
export default
|
|
2
|
-
|
|
3
|
-
id: '
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export default [
|
|
2
|
+
{
|
|
3
|
+
id: '29568',
|
|
4
|
+
product: {
|
|
5
|
+
id: 1093,
|
|
6
|
+
name: 'Jillian Top',
|
|
7
|
+
thumbnail: {
|
|
8
|
+
url:
|
|
9
|
+
'https://master-7rqtwti-c5v7sxvquxwl4.us-4.magentosite.cloud/media/catalog/product/cache/d3ba9f7bcd3b0724e976dc5144b29c7d/v/t/vt12-kh_main_2.jpg',
|
|
10
|
+
__typename: 'ProductImage'
|
|
11
|
+
},
|
|
12
|
+
__typename: 'ConfigurableProduct'
|
|
13
|
+
},
|
|
14
|
+
quantity: 3,
|
|
15
|
+
configurable_options: [
|
|
16
|
+
{
|
|
17
|
+
configurable_product_option_uid: 179,
|
|
18
|
+
option_label: 'Fashion Color',
|
|
19
|
+
configurable_product_option_value_uid: 18,
|
|
20
|
+
value_label: 'Peach',
|
|
21
|
+
__typename: 'SelectedConfigurableOption'
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
configurable_product_option_uid: 182,
|
|
25
|
+
option_label: 'Fashion Size',
|
|
26
|
+
configurable_product_option_value_uid: 27,
|
|
27
|
+
value_label: 'M',
|
|
28
|
+
__typename: 'SelectedConfigurableOption'
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
__typename: 'ConfigurableCartItem'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: '29570',
|
|
35
|
+
product: {
|
|
36
|
+
id: 1115,
|
|
37
|
+
name: 'Juno Sweater',
|
|
38
|
+
thumbnail: {
|
|
39
|
+
url:
|
|
40
|
+
'https://master-7rqtwti-c5v7sxvquxwl4.us-4.magentosite.cloud/media/catalog/product/cache/d3ba9f7bcd3b0724e976dc5144b29c7d/v/s/vsw02-pe_main_2.jpg',
|
|
41
|
+
__typename: 'ProductImage'
|
|
42
|
+
},
|
|
43
|
+
__typename: 'ConfigurableProduct'
|
|
44
|
+
},
|
|
45
|
+
quantity: 1,
|
|
46
|
+
configurable_options: [
|
|
47
|
+
{
|
|
48
|
+
configurable_product_option_uid: 179,
|
|
49
|
+
option_label: 'Fashion Color',
|
|
50
|
+
configurable_product_option_value_uid: 21,
|
|
51
|
+
value_label: 'Rain',
|
|
52
|
+
__typename: 'SelectedConfigurableOption'
|
|
53
|
+
},
|
|
6
54
|
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
__typename: '
|
|
55
|
+
configurable_product_option_uid: 182,
|
|
56
|
+
option_label: 'Fashion Size',
|
|
57
|
+
configurable_product_option_value_uid: 29,
|
|
58
|
+
value_label: 'XS',
|
|
59
|
+
__typename: 'SelectedConfigurableOption'
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
__typename: 'ConfigurableCartItem'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: '29572',
|
|
66
|
+
product: {
|
|
67
|
+
id: 1152,
|
|
68
|
+
name: 'Angelina Tank Dress',
|
|
69
|
+
thumbnail: {
|
|
70
|
+
url:
|
|
71
|
+
'https://master-7rqtwti-c5v7sxvquxwl4.us-4.magentosite.cloud/media/catalog/product/cache/d3ba9f7bcd3b0724e976dc5144b29c7d/v/d/vd01-ll_main_2.jpg',
|
|
72
|
+
__typename: 'ProductImage'
|
|
73
|
+
},
|
|
74
|
+
__typename: 'ConfigurableProduct'
|
|
75
|
+
},
|
|
76
|
+
quantity: 3,
|
|
77
|
+
configurable_options: [
|
|
78
|
+
{
|
|
79
|
+
configurable_product_option_uid: 179,
|
|
80
|
+
option_label: 'Fashion Color',
|
|
81
|
+
configurable_product_option_value_uid: 20,
|
|
82
|
+
value_label: 'Lilac',
|
|
83
|
+
__typename: 'SelectedConfigurableOption'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
configurable_product_option_uid: 182,
|
|
87
|
+
option_label: 'Fashion Size',
|
|
88
|
+
configurable_product_option_value_uid: 26,
|
|
89
|
+
value_label: 'L',
|
|
90
|
+
__typename: 'SelectedConfigurableOption'
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
__typename: 'ConfigurableCartItem'
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
export const singleItem = [
|
|
98
|
+
{
|
|
99
|
+
id: '29568',
|
|
100
|
+
product: {
|
|
101
|
+
id: 1093,
|
|
102
|
+
name: 'Jillian Top',
|
|
103
|
+
thumbnail: {
|
|
104
|
+
url:
|
|
105
|
+
'https://master-7rqtwti-c5v7sxvquxwl4.us-4.magentosite.cloud/media/catalog/product/cache/d3ba9f7bcd3b0724e976dc5144b29c7d/v/t/vt12-kh_main_2.jpg',
|
|
106
|
+
__typename: 'ProductImage'
|
|
36
107
|
},
|
|
108
|
+
__typename: 'ConfigurableProduct'
|
|
109
|
+
},
|
|
110
|
+
quantity: 3,
|
|
111
|
+
configurable_options: [
|
|
37
112
|
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
url:
|
|
44
|
-
'https://master-7rqtwti-c5v7sxvquxwl4.eu-4.magentosite.cloud/media/catalog/product/cache/d3ba9f7bcd3b0724e976dc5144b29c7d/v/s/vsw02-pe_main_2.jpg',
|
|
45
|
-
__typename: 'ProductImage'
|
|
46
|
-
},
|
|
47
|
-
__typename: 'ConfigurableProduct'
|
|
48
|
-
},
|
|
49
|
-
quantity: 1,
|
|
50
|
-
configurable_options: [
|
|
51
|
-
{
|
|
52
|
-
configurable_product_option_uid: 179,
|
|
53
|
-
option_label: 'Fashion Color',
|
|
54
|
-
configurable_product_option_value_uid: 21,
|
|
55
|
-
value_label: 'Rain',
|
|
56
|
-
__typename: 'SelectedConfigurableOption'
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
configurable_product_option_uid: 182,
|
|
60
|
-
option_label: 'Fashion Size',
|
|
61
|
-
configurable_product_option_value_uid: 29,
|
|
62
|
-
value_label: 'XS',
|
|
63
|
-
__typename: 'SelectedConfigurableOption'
|
|
64
|
-
}
|
|
65
|
-
],
|
|
66
|
-
__typename: 'ConfigurableCartItem'
|
|
113
|
+
configurable_product_option_uid: 179,
|
|
114
|
+
option_label: 'Fashion Color',
|
|
115
|
+
configurable_product_option_value_uid: 18,
|
|
116
|
+
value_label: 'Peach',
|
|
117
|
+
__typename: 'SelectedConfigurableOption'
|
|
67
118
|
},
|
|
68
119
|
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
url:
|
|
75
|
-
'https://master-7rqtwti-c5v7sxvquxwl4.eu-4.magentosite.cloud/media/catalog/product/cache/d3ba9f7bcd3b0724e976dc5144b29c7d/v/d/vd01-ll_main_2.jpg',
|
|
76
|
-
__typename: 'ProductImage'
|
|
77
|
-
},
|
|
78
|
-
__typename: 'ConfigurableProduct'
|
|
79
|
-
},
|
|
80
|
-
quantity: 3,
|
|
81
|
-
configurable_options: [
|
|
82
|
-
{
|
|
83
|
-
configurable_product_option_uid: 179,
|
|
84
|
-
option_label: 'Fashion Color',
|
|
85
|
-
configurable_product_option_value_uid: 20,
|
|
86
|
-
value_label: 'Lilac',
|
|
87
|
-
__typename: 'SelectedConfigurableOption'
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
configurable_product_option_uid: 182,
|
|
91
|
-
option_label: 'Fashion Size',
|
|
92
|
-
configurable_product_option_value_uid: 26,
|
|
93
|
-
value_label: 'L',
|
|
94
|
-
__typename: 'SelectedConfigurableOption'
|
|
95
|
-
}
|
|
96
|
-
],
|
|
97
|
-
__typename: 'ConfigurableCartItem'
|
|
120
|
+
configurable_product_option_uid: 182,
|
|
121
|
+
option_label: 'Fashion Size',
|
|
122
|
+
configurable_product_option_value_uid: 27,
|
|
123
|
+
value_label: 'M',
|
|
124
|
+
__typename: 'SelectedConfigurableOption'
|
|
98
125
|
}
|
|
99
126
|
],
|
|
100
|
-
__typename: '
|
|
127
|
+
__typename: 'ConfigurableCartItem'
|
|
101
128
|
}
|
|
102
|
-
|
|
129
|
+
];
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState, useCallback, useMemo } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { useQuery } from '@apollo/client';
|
|
3
3
|
|
|
4
|
-
import { useCartContext } from '../../../context/cart';
|
|
5
4
|
import mergeOperations from '../../../util/shallowMerge';
|
|
6
5
|
import DEFAULT_OPERATIONS from './itemsReview.gql';
|
|
7
6
|
|
|
@@ -9,9 +8,9 @@ export const useItemsReview = props => {
|
|
|
9
8
|
const [showAllItems, setShowAllItems] = useState(false);
|
|
10
9
|
const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
|
|
11
10
|
|
|
12
|
-
const {
|
|
11
|
+
const { getConfigurableThumbnailSource } = operations;
|
|
13
12
|
|
|
14
|
-
const
|
|
13
|
+
const { items: itemsData } = props;
|
|
15
14
|
|
|
16
15
|
const { data: configurableThumbnailSourceData } = useQuery(
|
|
17
16
|
getConfigurableThumbnailSource,
|
|
@@ -27,48 +26,29 @@ export const useItemsReview = props => {
|
|
|
27
26
|
}
|
|
28
27
|
}, [configurableThumbnailSourceData]);
|
|
29
28
|
|
|
30
|
-
const [
|
|
31
|
-
fetchItemsInCart,
|
|
32
|
-
{ data: queryData, error, loading }
|
|
33
|
-
] = useLazyQuery(getItemsInCart, {
|
|
34
|
-
fetchPolicy: 'cache-and-network'
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// If static data was provided, use that instead of query data.
|
|
38
|
-
const data = props.data || queryData;
|
|
39
|
-
|
|
40
29
|
const setShowAllItemsFlag = useCallback(() => setShowAllItems(true), [
|
|
41
30
|
setShowAllItems
|
|
42
31
|
]);
|
|
43
32
|
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (cartId && !props.data) {
|
|
46
|
-
fetchItemsInCart({
|
|
47
|
-
variables: {
|
|
48
|
-
cartId
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
}, [cartId, fetchItemsInCart, props.data]);
|
|
53
|
-
|
|
54
33
|
useEffect(() => {
|
|
55
34
|
/**
|
|
56
35
|
* If there are 2 or less than 2 items in cart
|
|
57
36
|
* set show all items to `true`.
|
|
58
37
|
*/
|
|
59
|
-
if (
|
|
38
|
+
if (itemsData && itemsData.length <= 2) {
|
|
60
39
|
setShowAllItems(true);
|
|
61
40
|
}
|
|
62
|
-
}, [
|
|
41
|
+
}, [itemsData]);
|
|
63
42
|
|
|
64
|
-
const items =
|
|
43
|
+
const items = itemsData || [];
|
|
65
44
|
|
|
66
|
-
const totalQuantity =
|
|
45
|
+
const totalQuantity = items.reduce(
|
|
46
|
+
(previousValue, currentValue) => previousValue + currentValue.quantity,
|
|
47
|
+
0
|
|
48
|
+
);
|
|
67
49
|
|
|
68
50
|
return {
|
|
69
|
-
isLoading: !!loading,
|
|
70
51
|
items,
|
|
71
|
-
hasErrors: !!error,
|
|
72
52
|
totalQuantity,
|
|
73
53
|
showAllItems,
|
|
74
54
|
setShowAllItems: setShowAllItemsFlag,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { gql } from '@apollo/client';
|
|
2
|
+
|
|
3
|
+
export const GET_ORDER_CONFIRMATION_DETAILS = gql`
|
|
4
|
+
query getOrderConfirmationDetails($orderNumber: String!) {
|
|
5
|
+
# eslint-disable-next-line @graphql-eslint/require-id-when-available
|
|
6
|
+
customer {
|
|
7
|
+
email
|
|
8
|
+
# eslint-disable-next-line @graphql-eslint/require-id-when-available
|
|
9
|
+
orders(filter: { number: { eq: $orderNumber } }) {
|
|
10
|
+
items {
|
|
11
|
+
id
|
|
12
|
+
shipping_address {
|
|
13
|
+
firstname
|
|
14
|
+
lastname
|
|
15
|
+
street
|
|
16
|
+
city
|
|
17
|
+
region
|
|
18
|
+
postcode
|
|
19
|
+
country_code
|
|
20
|
+
}
|
|
21
|
+
shipping_method
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
getOrderConfirmationDetailsQuery: GET_ORDER_CONFIRMATION_DETAILS
|
|
30
|
+
};
|
|
@@ -9,6 +9,7 @@ import { useGoogleReCaptcha } from '../../../hooks/useGoogleReCaptcha';
|
|
|
9
9
|
|
|
10
10
|
import DEFAULT_OPERATIONS from './createAccount.gql';
|
|
11
11
|
import { useEventingContext } from '../../../context/eventing';
|
|
12
|
+
import { useHistory } from 'react-router-dom';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Returns props necessary to render CreateAccount component. In particular this
|
|
@@ -95,6 +96,7 @@ export const useCreateAccount = props => {
|
|
|
95
96
|
formAction: 'createAccount'
|
|
96
97
|
});
|
|
97
98
|
|
|
99
|
+
const history = useHistory();
|
|
98
100
|
const handleSubmit = useCallback(
|
|
99
101
|
async formValues => {
|
|
100
102
|
setIsSubmitting(true);
|
|
@@ -158,6 +160,8 @@ export const useCreateAccount = props => {
|
|
|
158
160
|
if (onSubmit) {
|
|
159
161
|
onSubmit();
|
|
160
162
|
}
|
|
163
|
+
|
|
164
|
+
history.push('/account-information');
|
|
161
165
|
} catch (error) {
|
|
162
166
|
if (process.env.NODE_ENV !== 'production') {
|
|
163
167
|
console.error(error);
|
|
@@ -179,7 +183,8 @@ export const useCreateAccount = props => {
|
|
|
179
183
|
removeCart,
|
|
180
184
|
setToken,
|
|
181
185
|
signIn,
|
|
182
|
-
dispatch
|
|
186
|
+
dispatch,
|
|
187
|
+
history
|
|
183
188
|
]
|
|
184
189
|
);
|
|
185
190
|
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
1
2
|
import { useUserContext } from '../../../context/user';
|
|
3
|
+
import { setUserOnOrderSuccess } from '../../../store/actions/user/asyncActions';
|
|
4
|
+
import { useLazyQuery } from '@apollo/client';
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
import mergeOperations from '../../../util/shallowMerge';
|
|
7
|
+
import DEFAULT_OPERATIONS from './orderConfirmationPage.gql';
|
|
8
|
+
import { useDispatch } from 'react-redux';
|
|
9
|
+
|
|
10
|
+
export const flattenGuestCartData = data => {
|
|
11
|
+
if (!data) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
4
14
|
const { cart } = data;
|
|
5
15
|
const { shipping_addresses } = cart;
|
|
6
16
|
const address = shipping_addresses[0];
|
|
@@ -18,17 +28,79 @@ export const flatten = data => {
|
|
|
18
28
|
postcode: address.postcode,
|
|
19
29
|
region: address.region.label,
|
|
20
30
|
shippingMethod,
|
|
31
|
+
street: address.street
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const flattenCustomerOrderData = data => {
|
|
36
|
+
if (!data) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { customer } = data;
|
|
41
|
+
const order = customer?.orders?.items?.[0];
|
|
42
|
+
if (!order || !order.shipping_address) {
|
|
43
|
+
// Return an empty response if no valid order or shipping address exists
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const { shipping_address: address } = order;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
city: address.city,
|
|
50
|
+
country: address.country_code,
|
|
51
|
+
email: customer.email,
|
|
52
|
+
firstname: address.firstname,
|
|
53
|
+
lastname: address.lastname,
|
|
54
|
+
postcode: address.postcode,
|
|
55
|
+
region: address.region,
|
|
21
56
|
street: address.street,
|
|
22
|
-
|
|
57
|
+
shippingMethod: order.shipping_method
|
|
23
58
|
};
|
|
24
59
|
};
|
|
25
60
|
|
|
26
61
|
export const useOrderConfirmationPage = props => {
|
|
27
|
-
const
|
|
62
|
+
const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
|
|
63
|
+
const { getOrderConfirmationDetailsQuery } = operations;
|
|
64
|
+
|
|
28
65
|
const [{ isSignedIn }] = useUserContext();
|
|
29
66
|
|
|
67
|
+
const [
|
|
68
|
+
fetchOrderConfirmationDetails,
|
|
69
|
+
{ data: queryData, error, loading }
|
|
70
|
+
] = useLazyQuery(getOrderConfirmationDetailsQuery);
|
|
71
|
+
|
|
72
|
+
const flatData =
|
|
73
|
+
flattenGuestCartData(props.data) || flattenCustomerOrderData(queryData);
|
|
74
|
+
|
|
75
|
+
const dispatch = useDispatch();
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (props.orderNumber && !props.data) {
|
|
79
|
+
const orderNumber = props.orderNumber;
|
|
80
|
+
fetchOrderConfirmationDetails({
|
|
81
|
+
variables: {
|
|
82
|
+
orderNumber
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
dispatch(setUserOnOrderSuccess(true));
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
// Reset the flag when leaving the page
|
|
91
|
+
dispatch(setUserOnOrderSuccess(false));
|
|
92
|
+
};
|
|
93
|
+
}, [
|
|
94
|
+
props.orderNumber,
|
|
95
|
+
props.data,
|
|
96
|
+
fetchOrderConfirmationDetails,
|
|
97
|
+
dispatch
|
|
98
|
+
]);
|
|
99
|
+
|
|
30
100
|
return {
|
|
31
|
-
flatData
|
|
32
|
-
isSignedIn
|
|
101
|
+
flatData,
|
|
102
|
+
isSignedIn,
|
|
103
|
+
error,
|
|
104
|
+
loading
|
|
33
105
|
};
|
|
34
106
|
};
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
} from '@apollo/client';
|
|
8
8
|
import { useEventingContext } from '../../context/eventing';
|
|
9
9
|
|
|
10
|
+
import { useHistory } from 'react-router-dom';
|
|
11
|
+
|
|
10
12
|
import { useUserContext } from '../../context/user';
|
|
11
13
|
import { useCartContext } from '../../context/cart';
|
|
12
14
|
|
|
@@ -68,8 +70,8 @@ export const CHECKOUT_STEP = {
|
|
|
68
70
|
* }
|
|
69
71
|
*/
|
|
70
72
|
export const useCheckoutPage = (props = {}) => {
|
|
73
|
+
const history = useHistory();
|
|
71
74
|
const operations = mergeOperations(DEFAULT_OPERATIONS, props.operations);
|
|
72
|
-
|
|
73
75
|
const {
|
|
74
76
|
createCartMutation,
|
|
75
77
|
getCheckoutDetailsQuery,
|
|
@@ -249,6 +251,7 @@ export const useCheckoutPage = (props = {}) => {
|
|
|
249
251
|
});
|
|
250
252
|
setPlaceOrderButtonClicked(true);
|
|
251
253
|
setIsPlacingOrder(true);
|
|
254
|
+
localStorage.setItem('orderCount', '1');
|
|
252
255
|
}, [cartId, getOrderDetails]);
|
|
253
256
|
|
|
254
257
|
const handlePlaceOrderEnterKeyPress = useCallback(() => {
|
|
@@ -383,6 +386,16 @@ export const useCheckoutPage = (props = {}) => {
|
|
|
383
386
|
isPlacingOrder,
|
|
384
387
|
reviewOrderButtonClicked
|
|
385
388
|
]);
|
|
389
|
+
useEffect(() => {
|
|
390
|
+
if (isSignedIn && placeOrderData) {
|
|
391
|
+
history.push('/order-confirmation', {
|
|
392
|
+
orderNumber: placeOrderData.placeOrder.order.order_number,
|
|
393
|
+
items: cartItems
|
|
394
|
+
});
|
|
395
|
+
} else if (!isSignedIn && placeOrderData) {
|
|
396
|
+
history.push('/checkout');
|
|
397
|
+
}
|
|
398
|
+
}, [isSignedIn, placeOrderData, cartItems, history]);
|
|
386
399
|
|
|
387
400
|
return {
|
|
388
401
|
activeContent,
|
|
@@ -10,6 +10,12 @@ import { useGoogleReCaptcha } from '../../hooks/useGoogleReCaptcha';
|
|
|
10
10
|
|
|
11
11
|
import DEFAULT_OPERATIONS from './createAccount.gql';
|
|
12
12
|
import { useEventingContext } from '../../context/eventing';
|
|
13
|
+
import { useHistory, useLocation } from 'react-router-dom';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Routes to redirect from if used to create an account.
|
|
17
|
+
*/
|
|
18
|
+
const REDIRECT_FOR_ROUTES = ['/checkout', '/order-confirmation'];
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
21
|
* Returns props necessary to render CreateAccount component. In particular this
|
|
@@ -47,7 +53,7 @@ export const useCreateAccount = props => {
|
|
|
47
53
|
{ createCart, removeCart, getCartDetails }
|
|
48
54
|
] = useCartContext();
|
|
49
55
|
const [
|
|
50
|
-
{ isGettingDetails },
|
|
56
|
+
{ isGettingDetails, userOnOrderSuccess },
|
|
51
57
|
{ getUserDetails, setToken }
|
|
52
58
|
] = useUserContext();
|
|
53
59
|
|
|
@@ -112,6 +118,9 @@ export const useCreateAccount = props => {
|
|
|
112
118
|
};
|
|
113
119
|
}, [handleCancel]);
|
|
114
120
|
|
|
121
|
+
const history = useHistory();
|
|
122
|
+
const location = useLocation();
|
|
123
|
+
|
|
115
124
|
const handleSubmit = useCallback(
|
|
116
125
|
async formValues => {
|
|
117
126
|
setIsSubmitting(true);
|
|
@@ -188,6 +197,13 @@ export const useCreateAccount = props => {
|
|
|
188
197
|
if (onSubmit) {
|
|
189
198
|
onSubmit();
|
|
190
199
|
}
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
userOnOrderSuccess &&
|
|
203
|
+
REDIRECT_FOR_ROUTES.includes(location.pathname)
|
|
204
|
+
) {
|
|
205
|
+
history.push('/account-information');
|
|
206
|
+
}
|
|
191
207
|
} catch (error) {
|
|
192
208
|
if (process.env.NODE_ENV !== 'production') {
|
|
193
209
|
console.error(error);
|
|
@@ -213,7 +229,10 @@ export const useCreateAccount = props => {
|
|
|
213
229
|
getCartDetails,
|
|
214
230
|
fetchCartDetails,
|
|
215
231
|
onSubmit,
|
|
216
|
-
dispatch
|
|
232
|
+
dispatch,
|
|
233
|
+
history,
|
|
234
|
+
location.pathname,
|
|
235
|
+
userOnOrderSuccess
|
|
217
236
|
]
|
|
218
237
|
);
|
|
219
238
|
|
|
@@ -54,7 +54,7 @@ export const getStateFromSearch = (initialValue, filterKeys, filterItems) => {
|
|
|
54
54
|
|
|
55
55
|
if (existingFilter) {
|
|
56
56
|
items.add(existingFilter);
|
|
57
|
-
} else {
|
|
57
|
+
} else if (group !== 'price') {
|
|
58
58
|
console.warn(
|
|
59
59
|
`Existing filter ${value} not found in possible filters`
|
|
60
60
|
);
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { useCallback, useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { useLocation } from 'react-router-dom';
|
|
2
3
|
|
|
3
4
|
export const useFilterBlock = props => {
|
|
4
|
-
const { filterState, items, initialOpen } = props;
|
|
5
|
+
const { filterState, items, initialOpen, group } = props;
|
|
6
|
+
const location = useLocation();
|
|
5
7
|
|
|
6
8
|
const hasSelected = useMemo(() => {
|
|
9
|
+
const params = new URLSearchParams(location.search);
|
|
10
|
+
//expansion of price filter dropdown
|
|
11
|
+
if (group == 'price') {
|
|
12
|
+
return params.get('price[filter]') ? true : false;
|
|
13
|
+
}
|
|
7
14
|
return items.some(item => {
|
|
8
15
|
return filterState && filterState.has(item);
|
|
9
16
|
});
|
|
10
|
-
}, [filterState, items]);
|
|
17
|
+
}, [filterState, items, group, location.search]);
|
|
11
18
|
|
|
12
19
|
const [isExpanded, setExpanded] = useState(hasSelected || initialOpen);
|
|
13
20
|
|
|
@@ -182,9 +182,10 @@ export const useFilterSidebar = props => {
|
|
|
182
182
|
}, [handleClose]);
|
|
183
183
|
|
|
184
184
|
const handleReset = useCallback(() => {
|
|
185
|
-
filterApi.clear();
|
|
186
|
-
setIsApplying(true);
|
|
187
|
-
|
|
185
|
+
//filterApi.clear();
|
|
186
|
+
//setIsApplying(true);
|
|
187
|
+
history.replace({ search: 'page=1' });
|
|
188
|
+
}, [history]);
|
|
188
189
|
|
|
189
190
|
const handleKeyDownActions = useCallback(
|
|
190
191
|
event => {
|
|
@@ -14,7 +14,23 @@ export const useFormError = props => {
|
|
|
14
14
|
defaultMessage:
|
|
15
15
|
'An error has occurred. Please check the input and try again.'
|
|
16
16
|
});
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
const firstError = errors
|
|
19
|
+
.filter(error => error !== null || undefined)
|
|
20
|
+
.map(error => (Array.isArray(error) ? error[0] : error))
|
|
21
|
+
.find(message => message);
|
|
22
|
+
var graphqlErrorMessage;
|
|
23
|
+
|
|
24
|
+
if (firstError) {
|
|
25
|
+
graphqlErrorMessage = formatMessage({
|
|
26
|
+
id: 'formError.responseError',
|
|
27
|
+
defaultMessage: firstError.message
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return graphqlErrorMessage
|
|
32
|
+
? deriveErrorMessage(errors, graphqlErrorMessage)
|
|
33
|
+
: deriveErrorMessage(errors, defaultErrorMessage);
|
|
18
34
|
}, [errors, formatMessage, allowErrorMessages]);
|
|
19
35
|
|
|
20
36
|
return {
|
|
@@ -11,7 +11,7 @@ export const GET_CONFIGURABLE_THUMBNAIL_SOURCE = gql`
|
|
|
11
11
|
`;
|
|
12
12
|
|
|
13
13
|
export const GET_PRODUCT_THUMBNAILS_BY_URL_KEY = gql`
|
|
14
|
-
query GetProductThumbnailsByURLKey($urlKeys: [String
|
|
14
|
+
query GetProductThumbnailsByURLKey($urlKeys: [String]!) {
|
|
15
15
|
products(filter: { url_key: { in: $urlKeys } }) {
|
|
16
16
|
# eslint-disable-next-line @graphql-eslint/require-id-when-available
|
|
17
17
|
items {
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { gql } from '@apollo/client';
|
|
2
2
|
|
|
3
3
|
export const GET_PRODUCT_FILTERS_BY_CATEGORY = gql`
|
|
4
|
-
query getProductFiltersByCategory(
|
|
5
|
-
|
|
6
|
-
) {
|
|
7
|
-
products(filter: { category_uid: $categoryIdFilter }) {
|
|
4
|
+
query getProductFiltersByCategory($filters: ProductAttributeFilterInput!) {
|
|
5
|
+
products(filter: $filters) {
|
|
8
6
|
aggregations {
|
|
9
7
|
label
|
|
10
8
|
count
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useEffect, useState, useMemo } from 'react';
|
|
2
2
|
import { useLazyQuery, useQuery } from '@apollo/client';
|
|
3
3
|
|
|
4
4
|
import mergeOperations from '../../../util/shallowMerge';
|
|
@@ -29,6 +29,92 @@ export const useCategoryContent = props => {
|
|
|
29
29
|
getCategoryAvailableSortMethodsQuery
|
|
30
30
|
} = operations;
|
|
31
31
|
|
|
32
|
+
const [
|
|
33
|
+
getFiltersAttributeCode,
|
|
34
|
+
{ data: filterAttributeData }
|
|
35
|
+
] = useLazyQuery(getProductFiltersByCategoryQuery, {
|
|
36
|
+
fetchPolicy: 'cache-and-network',
|
|
37
|
+
nextFetchPolicy: 'cache-first'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (categoryId) {
|
|
42
|
+
getFiltersAttributeCode({
|
|
43
|
+
variables: {
|
|
44
|
+
filters: {
|
|
45
|
+
category_uid: { eq: categoryId }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}, [categoryId, getFiltersAttributeCode]);
|
|
51
|
+
|
|
52
|
+
const availableFilterData = filterAttributeData
|
|
53
|
+
? filterAttributeData.products?.aggregations
|
|
54
|
+
: null;
|
|
55
|
+
const availableFilters = availableFilterData
|
|
56
|
+
?.map(eachitem => eachitem.attribute_code)
|
|
57
|
+
?.sort();
|
|
58
|
+
|
|
59
|
+
const handlePriceFilter = priceFilter => {
|
|
60
|
+
if (priceFilter && priceFilter.size > 0) {
|
|
61
|
+
for (const price of priceFilter) {
|
|
62
|
+
const [from, to] = price.value.split('_');
|
|
63
|
+
return { price: { from, to } };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const [filterOptions, setFilterOptions] = useState();
|
|
70
|
+
|
|
71
|
+
const selectedFilters = useMemo(() => {
|
|
72
|
+
const filters = {};
|
|
73
|
+
if (filterOptions) {
|
|
74
|
+
for (const [group, items] of filterOptions.entries()) {
|
|
75
|
+
availableFilters?.map(eachitem => {
|
|
76
|
+
if (eachitem === group && group !== 'price') {
|
|
77
|
+
const sampleArray = [];
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
sampleArray.push(item.value);
|
|
80
|
+
}
|
|
81
|
+
filters[group] = sampleArray;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (filterOptions && filterOptions.has('price')) {
|
|
88
|
+
const priceFilter = filterOptions.get('price');
|
|
89
|
+
const priceRange = handlePriceFilter(priceFilter);
|
|
90
|
+
if (priceRange.price) {
|
|
91
|
+
filters.price = priceRange.price;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return filters;
|
|
96
|
+
}, [filterOptions, availableFilters]);
|
|
97
|
+
|
|
98
|
+
const dynamicQueryVariables = useMemo(() => {
|
|
99
|
+
const generateDynamicFiltersQuery = filterParams => {
|
|
100
|
+
let filterConditions = {
|
|
101
|
+
category_uid: { eq: categoryId }
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
Object.keys(filterParams).forEach(key => {
|
|
105
|
+
let filter = {};
|
|
106
|
+
if (key !== 'price') {
|
|
107
|
+
filter = { [key]: { in: filterParams[key] } };
|
|
108
|
+
}
|
|
109
|
+
filterConditions = { ...filterConditions, ...filter };
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return filterConditions;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return generateDynamicFiltersQuery(selectedFilters);
|
|
116
|
+
}, [selectedFilters, categoryId]);
|
|
117
|
+
|
|
32
118
|
const placeholderItems = Array.from({ length: pageSize }).fill(null);
|
|
33
119
|
|
|
34
120
|
const [getFilters, { data: filterData }] = useLazyQuery(
|
|
@@ -60,18 +146,26 @@ export const useCategoryContent = props => {
|
|
|
60
146
|
);
|
|
61
147
|
|
|
62
148
|
const [, { dispatch }] = useEventingContext();
|
|
63
|
-
|
|
149
|
+
const [previousFilters, setPreviousFilters] = useState(null);
|
|
64
150
|
useEffect(() => {
|
|
65
|
-
if (
|
|
151
|
+
if (
|
|
152
|
+
categoryId &&
|
|
153
|
+
JSON.stringify(selectedFilters) !== JSON.stringify(previousFilters)
|
|
154
|
+
) {
|
|
66
155
|
getFilters({
|
|
67
156
|
variables: {
|
|
68
|
-
|
|
69
|
-
eq: categoryId
|
|
70
|
-
}
|
|
157
|
+
filters: dynamicQueryVariables
|
|
71
158
|
}
|
|
72
159
|
});
|
|
160
|
+
setPreviousFilters(selectedFilters);
|
|
73
161
|
}
|
|
74
|
-
}, [
|
|
162
|
+
}, [
|
|
163
|
+
categoryId,
|
|
164
|
+
selectedFilters,
|
|
165
|
+
dynamicQueryVariables,
|
|
166
|
+
previousFilters,
|
|
167
|
+
getFilters
|
|
168
|
+
]);
|
|
75
169
|
|
|
76
170
|
useEffect(() => {
|
|
77
171
|
if (categoryId) {
|
|
@@ -85,7 +179,7 @@ export const useCategoryContent = props => {
|
|
|
85
179
|
}
|
|
86
180
|
}, [categoryId, getSortMethods]);
|
|
87
181
|
|
|
88
|
-
const filters = filterData ? filterData.products
|
|
182
|
+
const filters = filterData ? filterData.products?.aggregations : null;
|
|
89
183
|
const items = data ? data.products.items : placeholderItems;
|
|
90
184
|
const totalPagesFromData = data
|
|
91
185
|
? data.products.page_info.total_pages
|
|
@@ -104,7 +198,7 @@ export const useCategoryContent = props => {
|
|
|
104
198
|
: null;
|
|
105
199
|
|
|
106
200
|
useEffect(() => {
|
|
107
|
-
if (!categoryLoading && categoryData
|
|
201
|
+
if (!categoryLoading && categoryData?.categories.items.length > 0) {
|
|
108
202
|
dispatch({
|
|
109
203
|
type: 'CATEGORY_PAGE_VIEW',
|
|
110
204
|
payload: {
|
|
@@ -122,6 +216,8 @@ export const useCategoryContent = props => {
|
|
|
122
216
|
categoryName,
|
|
123
217
|
categoryDescription,
|
|
124
218
|
filters,
|
|
219
|
+
filterOptions,
|
|
220
|
+
setFilterOptions,
|
|
125
221
|
items,
|
|
126
222
|
totalCount,
|
|
127
223
|
totalPagesFromData
|
|
@@ -10,10 +10,15 @@ import { retrieveCartId } from '../../store/actions/cart';
|
|
|
10
10
|
|
|
11
11
|
import DEFAULT_OPERATIONS from './signIn.gql';
|
|
12
12
|
import { useEventingContext } from '../../context/eventing';
|
|
13
|
+
import { useHistory, useLocation } from 'react-router-dom';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Routes to redirect from if used to create an account.
|
|
17
|
+
*/
|
|
18
|
+
const REDIRECT_FOR_ROUTES = ['/checkout', '/order-confirmation'];
|
|
13
19
|
|
|
14
20
|
export const useSignIn = props => {
|
|
15
21
|
const {
|
|
16
|
-
handleTriggerClick,
|
|
17
22
|
getCartDetailsQuery,
|
|
18
23
|
setDefaultUsername,
|
|
19
24
|
showCreateAccount,
|
|
@@ -40,7 +45,7 @@ export const useSignIn = props => {
|
|
|
40
45
|
|
|
41
46
|
const userContext = useUserContext();
|
|
42
47
|
const [
|
|
43
|
-
{ isGettingDetails, getDetailsError },
|
|
48
|
+
{ isGettingDetails, getDetailsError, userOnOrderSuccess },
|
|
44
49
|
{ getUserDetails, setToken }
|
|
45
50
|
] = userContext;
|
|
46
51
|
|
|
@@ -83,10 +88,12 @@ export const useSignIn = props => {
|
|
|
83
88
|
const formApiRef = useRef(null);
|
|
84
89
|
const setFormApi = useCallback(api => (formApiRef.current = api), []);
|
|
85
90
|
|
|
91
|
+
const history = useHistory();
|
|
92
|
+
const location = useLocation();
|
|
93
|
+
|
|
86
94
|
const handleSubmit = useCallback(
|
|
87
95
|
async ({ email, password }) => {
|
|
88
96
|
setIsSigningIn(true);
|
|
89
|
-
handleTriggerClick();
|
|
90
97
|
|
|
91
98
|
try {
|
|
92
99
|
// Get source cart id (guest cart id).
|
|
@@ -144,6 +151,13 @@ export const useSignIn = props => {
|
|
|
144
151
|
});
|
|
145
152
|
|
|
146
153
|
getCartDetails({ fetchCartId, fetchCartDetails });
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
userOnOrderSuccess &&
|
|
157
|
+
REDIRECT_FOR_ROUTES.includes(location.pathname)
|
|
158
|
+
) {
|
|
159
|
+
history.push('/order-history');
|
|
160
|
+
}
|
|
147
161
|
} catch (error) {
|
|
148
162
|
if (process.env.NODE_ENV !== 'production') {
|
|
149
163
|
console.error(error);
|
|
@@ -168,7 +182,9 @@ export const useSignIn = props => {
|
|
|
168
182
|
getCartDetails,
|
|
169
183
|
fetchCartDetails,
|
|
170
184
|
dispatch,
|
|
171
|
-
|
|
185
|
+
history,
|
|
186
|
+
location.pathname,
|
|
187
|
+
userOnOrderSuccess
|
|
172
188
|
]
|
|
173
189
|
);
|
|
174
190
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import makeUrl from './makeUrl';
|
|
2
2
|
import resolveLinkProps from './resolveLinkProps';
|
|
3
|
-
|
|
3
|
+
import DOMPurify from 'dompurify';
|
|
4
4
|
/**
|
|
5
5
|
* Modifies html string images to use makeUrl as source and resolves links to use internal path.
|
|
6
6
|
*
|
|
@@ -9,7 +9,7 @@ import resolveLinkProps from './resolveLinkProps';
|
|
|
9
9
|
*/
|
|
10
10
|
const htmlStringImgUrlConverter = htmlString => {
|
|
11
11
|
const temporaryElement = document.createElement('div');
|
|
12
|
-
temporaryElement.innerHTML = htmlString;
|
|
12
|
+
temporaryElement.innerHTML = DOMPurify.sanitize(htmlString);
|
|
13
13
|
for (const imgElement of temporaryElement.getElementsByTagName('img')) {
|
|
14
14
|
imgElement.src = makeUrl(imgElement.src, {
|
|
15
15
|
type: 'image-wysiwyg',
|
package/lib/util/images.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// 4x5 transparent svg
|
|
2
2
|
// svg source = <svg xmlns='http://www.w3.org/2000/svg' width='4' height='5'><rect width='4' height='5' style='fill: none' /></svg>
|
|
3
3
|
export const transparentPlaceholder =
|
|
4
|
-
'data:image/svg+xml;base64,
|
|
4
|
+
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0IiBoZWlnaHQ9IjUiIHZpZXdCb3g9Ii0yNSAtMjUgMzEyIDMxMiI+CiAgICA8ZyBpZD0iTGF5ZXJfMi0yIj4KICAgICAgICA8cGF0aCBjbGFzcz0ic3QwIiBzdHlsZT0iZmlsbDogI2YwZjBmMDsgZmlsbC1ydWxlOiBldmVub2RkOyIgZD0iTTM2LDE4OS40di0xMDkuMWMwLTAuNywwLjQtMS42LDEuMS0ybDk0LjctNTcuMWMwLjctMC41LDEuNy0wLjUsMi41LDBsOTEuNSw1N2MwLjcsMC40LDEuMSwxLjEsMS4xLDJ2MTA3LjljMCwwLjctMC40LDEuNS0xLjEsMmwtMTkuMywxMi42Yy0xLjUsMS0zLjYsMC0zLjYtMnYtMTA1LjljMC0wLjctMC40LTEuNi0xLjEtMmwtNjcuNS00MS4zYy0wLjctMC41LTEuNi0wLjUtMi4zLDBsLTY5LjYsNDEuM2MtMC43LDAuNC0xLjEsMS4xLTEuMSwydjEwNS45YzAsMS45LTIsMy0zLjYsMmwtMjEuNS0xMy41aDB2LjJaTTg2LDExMC45djEwNi4zYzAsMC45LDAuNCwxLjYsMS4xLDJsNDQuNSwyNi43YzAuNywwLjUsMS43LDAuNCwyLjUsMGw0Mi41LTI2LjdjMC43LTAuNCwxLjEtMS4xLDEuMS0ydi0xMDUuOGMwLTAuNy0wLjQtMS41LTEuMS0ybC0yOC43LTE3LjljLTEuNS0xLTMuNiwwLjEtMy42LDJ2MTIxLjZjMCwwLjctMC40LDEuNS0xLjEsMmwtOS4zLDUuOWMtMC43LDAuNS0xLjYsMC41LTIuMywwbC0xMS4xLTUuOWMtMC43LTAuNC0xLjItMS4yLTEuMi0yLjF2LTEyMS43YzAtMS43LTItMy0zLjUtMmwtMjguOCwxNi4yYy0wLjcsMC40LTEuMSwxLjEtMS4xLDJ2MS41aC4xWiIvPgogICAgPC9nPgo8L3N2Zz4K';
|