@akinon/pz-theme 2.0.117-rc.0 → 2.0.118-beta.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/CHANGELOG.md +15 -35
- package/package.json +4 -4
- package/src/blocks/__tests__/locale-links.test.tsx +429 -0
- package/src/blocks/account-button-block.tsx +1 -1
- package/src/blocks/account-menu-block.tsx +9 -3
- package/src/blocks/applied-filters-block.tsx +7 -1
- package/src/blocks/auth-form-block.tsx +7 -4
- package/src/blocks/basket-items-block.tsx +1 -1
- package/src/blocks/basket-summary-block.tsx +1 -1
- package/src/blocks/button-block.tsx +5 -1
- package/src/blocks/carousel-item-block.tsx +1 -1
- package/src/blocks/coupon-wallet-block.tsx +3 -1
- package/src/blocks/facet-filters-block.tsx +14 -10
- package/src/blocks/favorite-button-block.tsx +1 -1
- package/src/blocks/favourites-grid-block.tsx +1 -1
- package/src/blocks/group-block.tsx +19 -6
- package/src/blocks/hamburger-menu-block.tsx +1 -1
- package/src/blocks/hotspot-block.tsx +3 -1
- package/src/blocks/hover-showcase-item-block.tsx +3 -1
- package/src/blocks/link-block.tsx +5 -22
- package/src/blocks/listing-breadcrumb-block.tsx +1 -1
- package/src/blocks/listing-pagination-block.tsx +11 -3
- package/src/blocks/mega-menu-block.tsx +9 -3
- package/src/blocks/mini-basket-block.tsx +1 -1
- package/src/blocks/nav-menu-block.tsx +9 -3
- package/src/blocks/oauth-buttons-block.tsx +4 -0
- package/src/blocks/order-detail-block.tsx +1 -1
- package/src/blocks/order-list-block.tsx +1 -1
- package/src/blocks/order-success-block.tsx +1 -1
- package/src/blocks/otp-form-block.tsx +4 -1
- package/src/blocks/payment-badges-block.tsx +1 -1
- package/src/blocks/product-breadcrumb-block.tsx +1 -1
- package/src/blocks/product-grid-block.tsx +1 -1
- package/src/blocks/product-recommendations-block.tsx +1 -1
- package/src/blocks/register-form-block.tsx +6 -3
- package/src/blocks/search-box-block.tsx +7 -1
- package/src/blocks/social-icons-list-block.tsx +1 -1
- package/src/blocks/social-links-block.tsx +1 -1
- package/src/blocks/sort-select-block.tsx +8 -2
- package/src/blocks/store-locator-block.tsx +3 -1
- package/src/blocks/text-block.tsx +8 -1
- package/src/blocks/variant-selector-block.tsx +6 -2
- package/src/blocks/wishlist-button-block.tsx +1 -1
- package/src/components/__tests__/theme-link.test.tsx +169 -0
- package/src/components/theme-link.tsx +36 -0
- package/src/custom/README-sections.md +6 -0
- package/src/custom/README.md +71 -0
- package/src/custom/__tests__/custom-entry-exports.test.tsx +13 -0
- package/src/custom/__tests__/theme-editable.test.tsx +179 -0
- package/src/custom/__tests__/use-theme-setting.test.tsx +101 -0
- package/src/custom/index.ts +16 -0
- package/src/custom/theme-editable.tsx +57 -4
- package/src/custom/use-theme-setting.ts +77 -10
- package/src/hooks/use-localize-href.ts +34 -0
- package/src/index.ts +12 -0
- package/src/sections/find-in-store-section.tsx +3 -1
- package/src/sections/hover-showcase-section.tsx +6 -2
- package/src/sections/installment-options-section.tsx +3 -1
- package/src/sections/slider-quick-view.tsx +1 -1
- package/src/utils/__tests__/localize-href.test.ts +328 -0
- package/src/utils/localize-href.ts +192 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,40 +1,20 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Updated dependencies [
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- Updated dependencies [bfafa3f49]
|
|
19
|
-
- Updated dependencies [57d7eb305]
|
|
20
|
-
- Updated dependencies [d99a6a7d5]
|
|
21
|
-
- Updated dependencies [9db81a714]
|
|
22
|
-
- Updated dependencies [591e345e1]
|
|
23
|
-
- Updated dependencies [4de5303c5]
|
|
24
|
-
- Updated dependencies [95b139dc1]
|
|
25
|
-
- Updated dependencies [1d00f2d06]
|
|
26
|
-
- Updated dependencies [4ac7b2a1e]
|
|
27
|
-
- Updated dependencies [e9598c71d]
|
|
28
|
-
- Updated dependencies [4998a9631]
|
|
29
|
-
- Updated dependencies [804d2bd6c]
|
|
30
|
-
- Updated dependencies [3909d3224]
|
|
31
|
-
- Updated dependencies [6a3d8a631]
|
|
32
|
-
- Updated dependencies [e18836b20]
|
|
33
|
-
- Updated dependencies [b1111d7b1]
|
|
34
|
-
- Updated dependencies [1595d45]
|
|
35
|
-
- Updated dependencies [dca1281]
|
|
36
|
-
- @akinon/next@2.0.117-rc.0
|
|
37
|
-
- @akinon/theme-contract@2.0.117-rc.0
|
|
3
|
+
## 2.0.118-beta.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [cbbbfd757]
|
|
8
|
+
- @akinon/next@2.0.118-beta.0
|
|
9
|
+
- @akinon/theme-contract@2.0.118-beta.0
|
|
10
|
+
|
|
11
|
+
## 2.0.117
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- bed36c7: ZERO-5100: Keep theme links in the language of the page they are on. Block links, router navigations and authored rich text now carry the active locale, so an English page no longer prefetches Turkish URLs and turns the shopper's session Turkish. Full URLs to the same storefront count as paths; the API, Next's assets and file addresses are left alone. Custom sections get ThemeLink and useLocalizeHref from @akinon/pz-theme/src/custom, ThemeEditable localizes the href of the anchor it wraps, and both ThemeEditable and useThemeSetting accept authored text written per language.
|
|
16
|
+
- @akinon/next@2.0.117
|
|
17
|
+
- @akinon/theme-contract@2.0.117
|
|
38
18
|
|
|
39
19
|
## 2.0.116
|
|
40
20
|
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.118-beta.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.
|
|
11
|
+
"@akinon/next": "2.0.118-beta.0",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.
|
|
16
|
+
"@akinon/theme-contract": "2.0.118-beta.0",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"react-multi-carousel": "2.8.4",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"zod": "4.4.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@akinon/next": "2.0.
|
|
24
|
+
"@akinon/next": "2.0.118-beta.0",
|
|
25
25
|
"@testing-library/jest-dom": "5.16.4",
|
|
26
26
|
"@testing-library/react": "16.3.2",
|
|
27
27
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZERO-5100: on an English page every internal link a block renders — and
|
|
3
|
+
* every navigation it makes itself — carries `/en`.
|
|
4
|
+
*
|
|
5
|
+
* Content stores storefront paths without a locale, so these used to point at
|
|
6
|
+
* the Turkish site: a click left the English site, and in production the
|
|
7
|
+
* prefetch of each unprefixed link made the middleware set `pz-locale=tr`,
|
|
8
|
+
* turning every later client request Turkish while the page stayed English.
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
12
|
+
import { renderToString } from 'react-dom/server';
|
|
13
|
+
|
|
14
|
+
const mockPush = jest.fn();
|
|
15
|
+
let mockPathname = '/en/';
|
|
16
|
+
|
|
17
|
+
jest.mock('next/navigation', () => ({
|
|
18
|
+
useRouter: () => ({ push: mockPush, replace: jest.fn() }),
|
|
19
|
+
usePathname: () => mockPathname,
|
|
20
|
+
useSearchParams: () => new URLSearchParams('')
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Turkish is the hidden default, English lives under /en — zsa-zsa-zsu's setup.
|
|
24
|
+
const TURKISH = {
|
|
25
|
+
locale: 'tr',
|
|
26
|
+
defaultLocaleValue: 'tr',
|
|
27
|
+
localeUrlStrategy: 'hide-default-locale'
|
|
28
|
+
};
|
|
29
|
+
let mockLocalization: Record<string, unknown> = { ...TURKISH };
|
|
30
|
+
|
|
31
|
+
jest.mock('@akinon/next/hooks', () => ({
|
|
32
|
+
__esModule: true,
|
|
33
|
+
useLocalization: () => ({ t: (path: string) => path, ...mockLocalization })
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
jest.mock('@akinon/next/utils', () => ({
|
|
37
|
+
__esModule: true,
|
|
38
|
+
...jest.requireActual('@akinon/next/utils'),
|
|
39
|
+
urlLocaleMatcherRegex: /^\/(en)(?=\/|$)/
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const mockMenu = [
|
|
43
|
+
{
|
|
44
|
+
pk: 1,
|
|
45
|
+
label: 'Bedroom',
|
|
46
|
+
url: '/yatak-odasi/',
|
|
47
|
+
children: [
|
|
48
|
+
{ pk: 2, label: 'Beds', url: '/yatak-odasi/yataklar/', children: [] }
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
pk: 3,
|
|
53
|
+
label: 'Lookbook',
|
|
54
|
+
url: 'https://lookbook.example.com/',
|
|
55
|
+
children: []
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// jest maps every `@akinon/next/data/client/*` to one module, so this mock
|
|
60
|
+
// stands in for `user` as well: the account menu needs its logout mutation.
|
|
61
|
+
jest.mock('@akinon/next/data/client/misc', () => ({
|
|
62
|
+
useGetMenuQuery: () => ({ data: mockMenu, isLoading: false }),
|
|
63
|
+
useLogoutMutation: () => [jest.fn(), { isLoading: false }]
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
import NavMenuBlock from '../nav-menu-block';
|
|
67
|
+
import SearchBoxBlock from '../search-box-block';
|
|
68
|
+
import AccountMenuBlock from '../account-menu-block';
|
|
69
|
+
import TextBlock from '../text-block';
|
|
70
|
+
import GroupBlock from '../group-block';
|
|
71
|
+
import HoverShowcaseSection from '../../sections/hover-showcase-section';
|
|
72
|
+
|
|
73
|
+
const mkBlock = (type: string, properties: Record<string, unknown> = {}) =>
|
|
74
|
+
({
|
|
75
|
+
id: `${type}-1`,
|
|
76
|
+
type,
|
|
77
|
+
label: type,
|
|
78
|
+
order: 0,
|
|
79
|
+
hidden: false,
|
|
80
|
+
value: '',
|
|
81
|
+
properties,
|
|
82
|
+
styles: {},
|
|
83
|
+
blocks: []
|
|
84
|
+
} as never);
|
|
85
|
+
|
|
86
|
+
const blockProps = {
|
|
87
|
+
placeholderId: 'header',
|
|
88
|
+
sectionId: 'header-main',
|
|
89
|
+
currentBreakpoint: 'desktop',
|
|
90
|
+
isDesigner: false
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const navHrefs = (root: ParentNode) =>
|
|
94
|
+
Array.from(root.querySelectorAll('a')).map((a) => a.getAttribute('href'));
|
|
95
|
+
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
mockLocalization = { ...TURKISH };
|
|
98
|
+
mockPathname = '/en/';
|
|
99
|
+
mockPush.mockClear();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('nav-menu links', () => {
|
|
103
|
+
it('carry /en on an English page, in the server HTML too', () => {
|
|
104
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
105
|
+
const nav = mkBlock('nav-menu', { showSubmenu: true });
|
|
106
|
+
|
|
107
|
+
const host = document.createElement('div');
|
|
108
|
+
host.innerHTML = renderToString(
|
|
109
|
+
<NavMenuBlock block={nav} {...blockProps} />
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(navHrefs(host)).toContain('/en/yatak-odasi/');
|
|
113
|
+
expect(navHrefs(host)).not.toContain('/yatak-odasi/');
|
|
114
|
+
// Somewhere else entirely: no locale to add.
|
|
115
|
+
expect(navHrefs(host)).toContain('https://lookbook.example.com/');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('stay unprefixed on the default locale', () => {
|
|
119
|
+
const { container } = render(
|
|
120
|
+
<NavMenuBlock block={mkBlock('nav-menu')} {...blockProps} />
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(navHrefs(container)).toContain('/yatak-odasi/');
|
|
124
|
+
expect(navHrefs(container)).not.toContain('/en/yatak-odasi/');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('search submit', () => {
|
|
129
|
+
const submitSearch = () => {
|
|
130
|
+
const { container } = render(
|
|
131
|
+
<SearchBoxBlock
|
|
132
|
+
block={mkBlock('search-box', { mode: 'inline' })}
|
|
133
|
+
{...blockProps}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
const input = container.querySelector(
|
|
137
|
+
'[data-search-input]'
|
|
138
|
+
) as HTMLInputElement;
|
|
139
|
+
|
|
140
|
+
fireEvent.change(input, { target: { value: 'yastık' } });
|
|
141
|
+
fireEvent.submit(container.querySelector('form') as HTMLFormElement);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
it('lands on the English results from an English page', () => {
|
|
145
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
146
|
+
|
|
147
|
+
submitSearch();
|
|
148
|
+
|
|
149
|
+
expect(mockPush).toHaveBeenCalledWith(
|
|
150
|
+
`/en/list?search_text=${encodeURIComponent('yastık')}`
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('keeps the authored path on the default locale', () => {
|
|
155
|
+
submitSearch();
|
|
156
|
+
|
|
157
|
+
expect(mockPush).toHaveBeenCalledWith(
|
|
158
|
+
`/list?search_text=${encodeURIComponent('yastık')}`
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// The pathname carries `/en` while the stored URLs do not, so the current
|
|
164
|
+
// entry has to be matched in its localized form.
|
|
165
|
+
describe('current-page highlighting on an English page', () => {
|
|
166
|
+
const currentHrefs = (root: ParentNode) =>
|
|
167
|
+
Array.from(root.querySelectorAll('a[aria-current="page"]')).map((a) =>
|
|
168
|
+
a.getAttribute('href')
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
it('marks the nav-menu entry of the open category', () => {
|
|
172
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
173
|
+
mockPathname = '/en/yatak-odasi';
|
|
174
|
+
|
|
175
|
+
const { container } = render(
|
|
176
|
+
<NavMenuBlock block={mkBlock('nav-menu')} {...blockProps} />
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(currentHrefs(container)).toEqual(['/en/yatak-odasi/']);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('keeps marking it on the default locale', () => {
|
|
183
|
+
mockPathname = '/yatak-odasi';
|
|
184
|
+
|
|
185
|
+
const { container } = render(
|
|
186
|
+
<NavMenuBlock block={mkBlock('nav-menu')} {...blockProps} />
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(currentHrefs(container)).toEqual(['/yatak-odasi/']);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('never widens a section match to the English home entry', () => {
|
|
193
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
194
|
+
mockPathname = '/en/yatak-odasi/yataklar';
|
|
195
|
+
const withHome = mockMenu.slice();
|
|
196
|
+
mockMenu.unshift({ pk: 9, label: 'Home', url: '/', children: [] });
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const { container } = render(
|
|
200
|
+
<NavMenuBlock
|
|
201
|
+
block={mkBlock('nav-menu', { activeMatch: 'section' })}
|
|
202
|
+
{...blockProps}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
expect(currentHrefs(container)).toEqual(['/en/yatak-odasi/']);
|
|
207
|
+
} finally {
|
|
208
|
+
mockMenu.splice(0, mockMenu.length, ...withHome);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('marks the account-menu entry of the open account page', () => {
|
|
213
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
214
|
+
mockPathname = '/en/account/orders';
|
|
215
|
+
|
|
216
|
+
const { container } = render(
|
|
217
|
+
<AccountMenuBlock block={mkBlock('account-menu')} {...blockProps} />
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
expect(currentHrefs(container)).toEqual(['/en/account/orders']);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('links drawn outside ThemeLink', () => {
|
|
225
|
+
it('prefixes the links inside authored rich text', () => {
|
|
226
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
227
|
+
const text = {
|
|
228
|
+
...(mkBlock('text') as object),
|
|
229
|
+
value: 'Detaylar için <a href="/kampanya/">tıklayın</a>'
|
|
230
|
+
} as never;
|
|
231
|
+
|
|
232
|
+
const host = document.createElement('div');
|
|
233
|
+
host.innerHTML = renderToString(<TextBlock block={text} {...blockProps} />);
|
|
234
|
+
|
|
235
|
+
expect(navHrefs(host)).toEqual(['/en/kampanya/']);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('prefixes the hover-showcase item links the section draws itself', () => {
|
|
239
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
240
|
+
const item = {
|
|
241
|
+
...(mkBlock('hover-showcase-item', {
|
|
242
|
+
title: 'Bedroom',
|
|
243
|
+
link: '/yatak-odasi/'
|
|
244
|
+
}) as object)
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const host = document.createElement('div');
|
|
248
|
+
host.innerHTML = renderToString(
|
|
249
|
+
<HoverShowcaseSection
|
|
250
|
+
section={
|
|
251
|
+
{
|
|
252
|
+
id: 'showcase',
|
|
253
|
+
type: 'hover-showcase',
|
|
254
|
+
label: 'showcase',
|
|
255
|
+
styles: {},
|
|
256
|
+
blocks: [item]
|
|
257
|
+
} as never
|
|
258
|
+
}
|
|
259
|
+
currentBreakpoint="desktop"
|
|
260
|
+
placeholderId="home"
|
|
261
|
+
isDesigner={false}
|
|
262
|
+
/>
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const hrefs = navHrefs(host);
|
|
266
|
+
expect(hrefs.length).toBeGreaterThan(0);
|
|
267
|
+
expect(new Set(hrefs)).toEqual(new Set(['/en/yatak-odasi/']));
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* A composed anchor is a group with `tag: 'a'`. Its href decides whether the
|
|
273
|
+
* click goes through the router — and the router link is the one that carries
|
|
274
|
+
* the locale, so an href that misses that branch loses `/en` with it.
|
|
275
|
+
*/
|
|
276
|
+
describe('group anchors', () => {
|
|
277
|
+
const groupAnchor = (href: string) =>
|
|
278
|
+
({
|
|
279
|
+
...(mkBlock('group', { tag: 'a', href }) as object)
|
|
280
|
+
} as never);
|
|
281
|
+
|
|
282
|
+
it('route a pasted path with its spaces still on it', () => {
|
|
283
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
284
|
+
|
|
285
|
+
const { container } = render(
|
|
286
|
+
<GroupBlock block={groupAnchor(' /yatak-odasi/ ')} {...blockProps} />
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
expect(navHrefs(container)).toEqual(['/en/yatak-odasi/']);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('keep that path unprefixed, and trimmed, on the default locale', () => {
|
|
293
|
+
const { container } = render(
|
|
294
|
+
<GroupBlock block={groupAnchor(' /yatak-odasi/ ')} {...blockProps} />
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
expect(navHrefs(container)).toEqual(['/yatak-odasi/']);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('leave an address somewhere else on its plain anchor', () => {
|
|
301
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
302
|
+
|
|
303
|
+
const { container } = render(
|
|
304
|
+
<GroupBlock
|
|
305
|
+
block={groupAnchor('https://lookbook.example.com/')}
|
|
306
|
+
{...blockProps}
|
|
307
|
+
/>
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
expect(navHrefs(container)).toEqual(['https://lookbook.example.com/']);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* A full URL of this storefront's own is one of its routes too, which is
|
|
315
|
+
* what `link-block` already made of it. Without this the merchant's pasted
|
|
316
|
+
* address bar fell to the plain anchor, and the click left the English site.
|
|
317
|
+
*/
|
|
318
|
+
describe('with a full URL of this storefront', () => {
|
|
319
|
+
const configured = process.env.NEXT_PUBLIC_URL;
|
|
320
|
+
|
|
321
|
+
beforeEach(() => {
|
|
322
|
+
process.env.NEXT_PUBLIC_URL = 'https://www.zsazsazsu.com.tr';
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
afterAll(() => {
|
|
326
|
+
process.env.NEXT_PUBLIC_URL = configured;
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('routes it as the localized path on an English page', () => {
|
|
330
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
331
|
+
|
|
332
|
+
const { container } = render(
|
|
333
|
+
<GroupBlock
|
|
334
|
+
block={groupAnchor('https://zsazsazsu.com.tr/yeni-urunler/')}
|
|
335
|
+
{...blockProps}
|
|
336
|
+
/>
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
expect(navHrefs(container)).toEqual(['/en/yeni-urunler/']);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('routes it as the bare path on the default locale', () => {
|
|
343
|
+
const { container } = render(
|
|
344
|
+
<GroupBlock
|
|
345
|
+
block={groupAnchor('https://www.zsazsazsu.com.tr/yeni-urunler/')}
|
|
346
|
+
{...blockProps}
|
|
347
|
+
/>
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
expect(navHrefs(container)).toEqual(['/yeni-urunler/']);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// A file keeps the path it points at and gains no locale, exactly as a
|
|
354
|
+
// root-relative '/files/kvkk.pdf' has always been treated here.
|
|
355
|
+
it('adds no locale to a file on this storefront', () => {
|
|
356
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
357
|
+
|
|
358
|
+
const { container } = render(
|
|
359
|
+
<GroupBlock
|
|
360
|
+
block={groupAnchor('https://www.zsazsazsu.com.tr/files/kvkk.pdf')}
|
|
361
|
+
{...blockProps}
|
|
362
|
+
/>
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
expect(navHrefs(container)).toEqual(['/files/kvkk.pdf']);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Merchants paste the address bar rather than typing a path: the live home
|
|
372
|
+
* composition stores 42 of its links as full URLs to this same site. In
|
|
373
|
+
* production Next prefetches those as internal routes, and the middleware
|
|
374
|
+
* answers the unprefixed path with `pz-locale=tr`.
|
|
375
|
+
*/
|
|
376
|
+
describe('full URLs to this storefront', () => {
|
|
377
|
+
const configured = process.env.NEXT_PUBLIC_URL;
|
|
378
|
+
|
|
379
|
+
beforeEach(() => {
|
|
380
|
+
process.env.NEXT_PUBLIC_URL = 'https://www.zsazsazsu.com.tr';
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
afterAll(() => {
|
|
384
|
+
process.env.NEXT_PUBLIC_URL = configured;
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const richText = (markup: string) =>
|
|
388
|
+
({
|
|
389
|
+
...(mkBlock('text') as object),
|
|
390
|
+
value: markup
|
|
391
|
+
} as never);
|
|
392
|
+
|
|
393
|
+
const markup =
|
|
394
|
+
'<a href="https://www.zsazsazsu.com.tr/kampanya/">kampanya</a>' +
|
|
395
|
+
'<a href="https://zsazsazsu.com.tr/yeni-urunler/">yeni</a>' +
|
|
396
|
+
'<a href="https://www.zsazsazsu.com.tr/files/kvkk.pdf">kvkk</a>' +
|
|
397
|
+
'<a href="https://lookbook.example.com/">lookbook</a>';
|
|
398
|
+
|
|
399
|
+
it('become localized paths in authored rich text', () => {
|
|
400
|
+
mockLocalization = { ...TURKISH, locale: 'en' };
|
|
401
|
+
|
|
402
|
+
const host = document.createElement('div');
|
|
403
|
+
host.innerHTML = renderToString(
|
|
404
|
+
<TextBlock block={richText(markup)} {...blockProps} />
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
expect(navHrefs(host)).toEqual([
|
|
408
|
+
'/en/kampanya/',
|
|
409
|
+
'/en/yeni-urunler/',
|
|
410
|
+
// No localized copy of a file, the same as `/manifest.json`.
|
|
411
|
+
'https://www.zsazsazsu.com.tr/files/kvkk.pdf',
|
|
412
|
+
'https://lookbook.example.com/'
|
|
413
|
+
]);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('stay byte for byte as the merchant wrote them on the Turkish page', () => {
|
|
417
|
+
const host = document.createElement('div');
|
|
418
|
+
host.innerHTML = renderToString(
|
|
419
|
+
<TextBlock block={richText(markup)} {...blockProps} />
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
expect(navHrefs(host)).toEqual([
|
|
423
|
+
'https://www.zsazsazsu.com.tr/kampanya/',
|
|
424
|
+
'https://zsazsazsu.com.tr/yeni-urunler/',
|
|
425
|
+
'https://www.zsazsazsu.com.tr/files/kvkk.pdf',
|
|
426
|
+
'https://lookbook.example.com/'
|
|
427
|
+
]);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import NextLink from '
|
|
4
|
+
import { ThemeLink as NextLink } from '../components/theme-link';
|
|
5
5
|
import { useSession } from 'next-auth/react';
|
|
6
6
|
import { usePathname } from 'next/navigation';
|
|
7
7
|
import { getResponsiveValue, partAttrs } from '../utils';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import NextLink from '
|
|
4
|
+
import { ThemeLink as NextLink } from '../components/theme-link';
|
|
5
5
|
import { usePathname } from 'next/navigation';
|
|
6
|
+
import { useLocalizeHref } from '../hooks/use-localize-href';
|
|
6
7
|
import { signOut } from 'next-auth/react';
|
|
7
8
|
import { useLogoutMutation } from '@akinon/next/data/client/user';
|
|
8
9
|
import { emitCommerceEvent } from '../utils/analytics';
|
|
@@ -67,6 +68,7 @@ const AccountMenuBlock = ({
|
|
|
67
68
|
const emptyLabel = prop('emptyLabel', '');
|
|
68
69
|
|
|
69
70
|
const pathname = usePathname();
|
|
71
|
+
const localize = useLocalizeHref();
|
|
70
72
|
const [logout, { isLoading: isLoggingOut }] = useLogoutMutation();
|
|
71
73
|
|
|
72
74
|
const definitions: Record<
|
|
@@ -114,15 +116,19 @@ const AccountMenuBlock = ({
|
|
|
114
116
|
.map((key) => definitions[key])
|
|
115
117
|
.filter((item) => Boolean(item?.href) && Boolean(item?.label));
|
|
116
118
|
|
|
119
|
+
// The pathname carries the locale (`/en/account/orders`) and the configured
|
|
120
|
+
// hrefs do not, so each item is matched in the localized form its link renders.
|
|
117
121
|
const isActive = (href: string) => {
|
|
118
122
|
if (!pathname || !href) return false;
|
|
123
|
+
const target = localize(href);
|
|
119
124
|
if (activeMatch === 'startsWith') {
|
|
120
125
|
return (
|
|
121
|
-
pathname ===
|
|
126
|
+
pathname === target ||
|
|
127
|
+
pathname.startsWith(`${target.replace(/\/$/, '')}/`)
|
|
122
128
|
);
|
|
123
129
|
}
|
|
124
130
|
|
|
125
|
-
return pathname ===
|
|
131
|
+
return pathname === target;
|
|
126
132
|
};
|
|
127
133
|
|
|
128
134
|
const handleLogout = async () => {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
5
5
|
import { getResponsiveValue, partAttrs } from '../utils';
|
|
6
|
+
import { useLocalizeHref } from '../hooks/use-localize-href';
|
|
6
7
|
import { hasItemTemplate, ItemTemplateList } from './item-template';
|
|
7
8
|
import { BlockRendererProps } from './block-renderer-registry';
|
|
8
9
|
import {
|
|
@@ -48,6 +49,7 @@ const AppliedFiltersBlock = ({
|
|
|
48
49
|
const router = useRouter();
|
|
49
50
|
const pathname = usePathname();
|
|
50
51
|
const searchParams = useSearchParams();
|
|
52
|
+
const localize = useLocalizeHref();
|
|
51
53
|
|
|
52
54
|
const showClearAll = readToggle(
|
|
53
55
|
block.properties?.showClearAll,
|
|
@@ -121,7 +123,11 @@ const AppliedFiltersBlock = ({
|
|
|
121
123
|
const hrefForParams = (params: URLSearchParams) => {
|
|
122
124
|
// Any filter change invalidates the current page cursor.
|
|
123
125
|
params.delete(pageQueryParam);
|
|
124
|
-
|
|
126
|
+
// Commerce's `listing.path` has no locale; `localize` adds the page's (and
|
|
127
|
+
// leaves the one a `pathname` already carries).
|
|
128
|
+
return localize(
|
|
129
|
+
buildListingHref(pathname || listing?.path || '', params)
|
|
130
|
+
);
|
|
125
131
|
};
|
|
126
132
|
|
|
127
133
|
/**
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useContext, useRef, useState } from 'react';
|
|
4
|
-
import NextLink from '
|
|
4
|
+
import { ThemeLink as NextLink } from '../components/theme-link';
|
|
5
5
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
6
6
|
import { signIn, useSession } from 'next-auth/react';
|
|
7
7
|
import { useLocalization } from '@akinon/next/hooks';
|
|
8
8
|
import { getResponsiveValue, partAttrs, resolveOption } from '../utils';
|
|
9
|
+
import { useLocalizeHref } from '../hooks/use-localize-href';
|
|
9
10
|
import {
|
|
10
11
|
AuthFieldErrors,
|
|
11
12
|
AuthFieldErrorsProvider,
|
|
@@ -298,6 +299,7 @@ const AuthFormBlock = ({
|
|
|
298
299
|
);
|
|
299
300
|
|
|
300
301
|
const router = useRouter();
|
|
302
|
+
const localize = useLocalizeHref();
|
|
301
303
|
const search = useSearchParams();
|
|
302
304
|
const pathname = usePathname();
|
|
303
305
|
const { status } = useSession();
|
|
@@ -395,8 +397,8 @@ const AuthFormBlock = ({
|
|
|
395
397
|
React.useEffect(() => {
|
|
396
398
|
if (!isSignedIn || whenSignedIn !== 'redirect') return;
|
|
397
399
|
|
|
398
|
-
router.replace(resolveSignedInDestination(signedInHref, search));
|
|
399
|
-
}, [isSignedIn, whenSignedIn, signedInHref, router, search]);
|
|
400
|
+
router.replace(localize(resolveSignedInDestination(signedInHref, search)));
|
|
401
|
+
}, [isSignedIn, whenSignedIn, signedInHref, router, search, localize]);
|
|
400
402
|
|
|
401
403
|
/**
|
|
402
404
|
* The form's contents, wrapped so a Captcha block (or the form's own slot)
|
|
@@ -607,7 +609,8 @@ const AuthFormBlock = ({
|
|
|
607
609
|
// Our own resolved path rather than next-auth's echo of it: the response
|
|
608
610
|
// url is whatever we handed in, and this one has been through the
|
|
609
611
|
// same-origin check. `null` is `stay` — there is nowhere to go.
|
|
610
|
-
|
|
612
|
+
// An authored destination has no locale; a returned one keeps its own.
|
|
613
|
+
if (destination) router.push(localize(destination));
|
|
611
614
|
} catch {
|
|
612
615
|
setError(errorLabel);
|
|
613
616
|
} finally {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useEffect, useState } from 'react';
|
|
4
4
|
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
import { useLocalizeHref } from '../hooks/use-localize-href';
|
|
5
6
|
import { useEmailSubscriptionMutation } from '@akinon/next/data/client/misc';
|
|
6
7
|
import { Modal } from '@akinon/next/components/modal';
|
|
7
8
|
import {
|
|
@@ -26,6 +27,7 @@ const ButtonBlock = ({
|
|
|
26
27
|
const action = useBlockAction(block, { currentBreakpoint, isDesigner });
|
|
27
28
|
|
|
28
29
|
const { locale } = useLocalization();
|
|
30
|
+
const localize = useLocalizeHref();
|
|
29
31
|
const [emailSubscription] = useEmailSubscriptionMutation();
|
|
30
32
|
const [feedbackModal, setFeedbackModal] = useState({
|
|
31
33
|
open: false,
|
|
@@ -677,7 +679,9 @@ const ButtonBlock = ({
|
|
|
677
679
|
return (
|
|
678
680
|
<>
|
|
679
681
|
<a
|
|
680
|
-
|
|
682
|
+
// A plain anchor, so the locale ThemeLink gives other links is added
|
|
683
|
+
// here: an authored `/yatak-odasi/` is the Turkish page.
|
|
684
|
+
href={localize(url)}
|
|
681
685
|
target={target}
|
|
682
686
|
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
|
|
683
687
|
{...partAttrs('root')}
|