@akinon/projectzero 2.0.6-rc.0 → 2.0.6-rc.2
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 +8 -0
- package/app-template/.lintstagedrc.js +4 -6
- package/app-template/CHANGELOG.md +81 -0
- package/app-template/eslint.config.mjs +14 -0
- package/app-template/next.config.mjs +26 -4
- package/app-template/package.json +34 -35
- package/app-template/public/locales/en/basket.json +4 -1
- package/app-template/public/locales/en/common.json +15 -0
- package/app-template/public/locales/en/forgot_password.json +4 -1
- package/app-template/public/locales/en/product.json +3 -0
- package/app-template/public/locales/tr/basket.json +4 -1
- package/app-template/public/locales/tr/common.json +15 -0
- package/app-template/public/locales/tr/forgot_password.json +4 -1
- package/app-template/public/locales/tr/product.json +3 -0
- package/app-template/src/__tests__/middleware-matcher.test.ts +1 -1
- package/app-template/src/app/[pz]/flat-page/[pk]/page.tsx +58 -2
- package/app-template/src/app/[pz]/group-product/[pk]/page.tsx +2 -2
- package/app-template/src/app/[pz]/layout.tsx +11 -2
- package/app-template/src/app/[pz]/product/[pk]/page.tsx +2 -2
- package/app-template/src/app/[pz]/template.tsx +3 -2
- package/app-template/src/app/manifest.ts +39 -0
- package/app-template/src/app/sw.ts +25 -0
- package/app-template/src/components/index.ts +2 -0
- package/app-template/src/components/install-prompt.tsx +93 -0
- package/app-template/src/components/offline-fallback.tsx +50 -0
- package/app-template/src/components/pwa-tags.tsx +1 -1
- package/app-template/src/hooks/use-fav-button.tsx +12 -5
- package/app-template/src/hooks/use-install-prompt.ts +146 -0
- package/app-template/src/hooks/use-online-status.ts +40 -0
- package/app-template/src/utils/generate-jsonld.ts +3 -0
- package/app-template/src/views/basket/basket-item.tsx +8 -3
- package/app-template/src/views/product/product-actions.tsx +4 -3
- package/app-template/tailwind.config.js +22 -0
- package/package.json +1 -1
- package/app-template/.eslintignore +0 -1
- package/app-template/.eslintrc.js +0 -10
- package/app-template/public/manifest.json +0 -35
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
|
|
3
|
-
const buildEslintCommand = (filenames) =>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
.
|
|
7
|
-
.join(' --file ')} --fix`;
|
|
8
|
-
};
|
|
3
|
+
const buildEslintCommand = (filenames) =>
|
|
4
|
+
`eslint --fix ${filenames
|
|
5
|
+
.map((f) => `"${path.relative(process.cwd(), f)}"`)
|
|
6
|
+
.join(' ')}`;
|
|
9
7
|
|
|
10
8
|
module.exports = {
|
|
11
9
|
'**/*.(ts|tsx)': () => 'yarn tsc --noEmit',
|
|
@@ -1,5 +1,86 @@
|
|
|
1
1
|
# projectzeronext
|
|
2
2
|
|
|
3
|
+
## 2.0.6-rc.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- c6edd8e2: ZERO-4389: Add headers configuration for service worker with security policies
|
|
8
|
+
- 1a345c47: ZERO-4234: Add Global Toast Notification System
|
|
9
|
+
- a943fd69: ZERO-4383: Add offline fallback component and localization support for offline messages
|
|
10
|
+
- b8c210d5: ZERO-4433: Update pipeline scripts to install git for build steps and correct route import path
|
|
11
|
+
- 6a62ffaa: ZERO-4390: Add serializeJsonLd utility and update JSON-LD handling in templates
|
|
12
|
+
- c1dfaae3: ZERO-4380: Add service worker implementation with Serwist for caching and precaching
|
|
13
|
+
- 89ed4e03: ZERO-4394: Add Next.js 16 ESLint migration v9 + flat config
|
|
14
|
+
- e7861d74: ZERO-4379: Refactor PWA manifest handling: replace manifest.json with manifest.webmanifest
|
|
15
|
+
- d64e1035: ZERO-4386: Add install prompt and localization for PWA installation
|
|
16
|
+
- 8d8fefbe: ZERO-4398: Enhance SEO metadata generation for flat pages and update FlatPage interface to include localized URLs
|
|
17
|
+
- Updated dependencies [89deabe5]
|
|
18
|
+
- Updated dependencies [1a345c47]
|
|
19
|
+
- Updated dependencies [1f1ae44e]
|
|
20
|
+
- Updated dependencies [8ae85c5a]
|
|
21
|
+
- Updated dependencies [89ed4e03]
|
|
22
|
+
- Updated dependencies [8d8fefbe]
|
|
23
|
+
- @akinon/next@2.0.6-rc.2
|
|
24
|
+
- @akinon/pz-theme@2.0.6-rc.2
|
|
25
|
+
- @akinon/pz-virtual-try-on@2.0.6-rc.2
|
|
26
|
+
- @akinon/pz-akifast@2.0.6-rc.2
|
|
27
|
+
- @akinon/pz-apple-pay@2.0.6-rc.2
|
|
28
|
+
- @akinon/pz-b2b@2.0.6-rc.2
|
|
29
|
+
- @akinon/pz-basket-gift-pack@2.0.6-rc.2
|
|
30
|
+
- @akinon/pz-bkm@2.0.6-rc.2
|
|
31
|
+
- @akinon/pz-checkout-gift-pack@2.0.6-rc.2
|
|
32
|
+
- @akinon/pz-click-collect@2.0.6-rc.2
|
|
33
|
+
- @akinon/pz-credit-payment@2.0.6-rc.2
|
|
34
|
+
- @akinon/pz-cybersource-uc@2.0.6-rc.2
|
|
35
|
+
- @akinon/pz-flow-payment@2.0.6-rc.2
|
|
36
|
+
- @akinon/pz-google-pay@2.0.6-rc.2
|
|
37
|
+
- @akinon/pz-gpay@2.0.6-rc.2
|
|
38
|
+
- @akinon/pz-haso@2.0.6-rc.2
|
|
39
|
+
- @akinon/pz-hepsipay@2.0.6-rc.2
|
|
40
|
+
- @akinon/pz-masterpass@2.0.6-rc.2
|
|
41
|
+
- @akinon/pz-masterpass-rest@2.0.6-rc.2
|
|
42
|
+
- @akinon/pz-multi-basket@2.0.6-rc.2
|
|
43
|
+
- @akinon/pz-one-click-checkout@2.0.6-rc.2
|
|
44
|
+
- @akinon/pz-otp@2.0.6-rc.2
|
|
45
|
+
- @akinon/pz-pay-on-delivery@2.0.6-rc.2
|
|
46
|
+
- @akinon/pz-saved-card@2.0.6-rc.2
|
|
47
|
+
- @akinon/pz-similar-products@2.0.6-rc.2
|
|
48
|
+
- @akinon/pz-tabby-extension@2.0.6-rc.2
|
|
49
|
+
- @akinon/pz-tamara-extension@2.0.6-rc.2
|
|
50
|
+
|
|
51
|
+
## 2.0.6-rc.1
|
|
52
|
+
|
|
53
|
+
### Patch Changes
|
|
54
|
+
|
|
55
|
+
- Updated dependencies [51ea0688]
|
|
56
|
+
- @akinon/next@2.0.6-rc.1
|
|
57
|
+
- @akinon/pz-theme@2.0.6-rc.1
|
|
58
|
+
- @akinon/pz-virtual-try-on@2.0.6-rc.1
|
|
59
|
+
- @akinon/pz-akifast@2.0.6-rc.1
|
|
60
|
+
- @akinon/pz-apple-pay@2.0.6-rc.1
|
|
61
|
+
- @akinon/pz-b2b@2.0.6-rc.1
|
|
62
|
+
- @akinon/pz-basket-gift-pack@2.0.6-rc.1
|
|
63
|
+
- @akinon/pz-bkm@2.0.6-rc.1
|
|
64
|
+
- @akinon/pz-checkout-gift-pack@2.0.6-rc.1
|
|
65
|
+
- @akinon/pz-click-collect@2.0.6-rc.1
|
|
66
|
+
- @akinon/pz-credit-payment@2.0.6-rc.1
|
|
67
|
+
- @akinon/pz-cybersource-uc@2.0.6-rc.1
|
|
68
|
+
- @akinon/pz-flow-payment@2.0.6-rc.1
|
|
69
|
+
- @akinon/pz-google-pay@2.0.6-rc.1
|
|
70
|
+
- @akinon/pz-gpay@2.0.6-rc.1
|
|
71
|
+
- @akinon/pz-haso@2.0.6-rc.1
|
|
72
|
+
- @akinon/pz-hepsipay@2.0.6-rc.1
|
|
73
|
+
- @akinon/pz-masterpass@2.0.6-rc.1
|
|
74
|
+
- @akinon/pz-masterpass-rest@2.0.6-rc.1
|
|
75
|
+
- @akinon/pz-multi-basket@2.0.6-rc.1
|
|
76
|
+
- @akinon/pz-one-click-checkout@2.0.6-rc.1
|
|
77
|
+
- @akinon/pz-otp@2.0.6-rc.1
|
|
78
|
+
- @akinon/pz-pay-on-delivery@2.0.6-rc.1
|
|
79
|
+
- @akinon/pz-saved-card@2.0.6-rc.1
|
|
80
|
+
- @akinon/pz-similar-products@2.0.6-rc.1
|
|
81
|
+
- @akinon/pz-tabby-extension@2.0.6-rc.1
|
|
82
|
+
- @akinon/pz-tamara-extension@2.0.6-rc.1
|
|
83
|
+
|
|
3
84
|
## 2.0.6-rc.0
|
|
4
85
|
|
|
5
86
|
### Patch Changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
2
|
+
import baseConfig from '@akinon/next/eslint.config.base.mjs'
|
|
3
|
+
|
|
4
|
+
export default defineConfig([
|
|
5
|
+
...baseConfig,
|
|
6
|
+
{
|
|
7
|
+
settings: {
|
|
8
|
+
next: {
|
|
9
|
+
rootDir: ['src/*/']
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
globalIgnores(['public/**'])
|
|
14
|
+
])
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import withSerwistInit from '@serwist/next';
|
|
2
2
|
import { withSentryConfig } from '@sentry/nextjs';
|
|
3
3
|
import withPzConfig from '@akinon/next/with-pz-config.js';
|
|
4
4
|
|
|
@@ -15,11 +15,33 @@ const nextConfig = {
|
|
|
15
15
|
destination: '/:pz/xml-sitemap/:node'
|
|
16
16
|
}
|
|
17
17
|
];
|
|
18
|
+
},
|
|
19
|
+
headers: async () => {
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
source: '/sw.js',
|
|
23
|
+
headers: [
|
|
24
|
+
{
|
|
25
|
+
key: 'Content-Type',
|
|
26
|
+
value: 'application/javascript; charset=utf-8'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: 'Cache-Control',
|
|
30
|
+
value: 'no-cache, no-store, must-revalidate'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: 'Content-Security-Policy',
|
|
34
|
+
value: "default-src 'self'; script-src 'self'"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
];
|
|
18
39
|
}
|
|
19
40
|
};
|
|
20
41
|
|
|
21
|
-
const
|
|
22
|
-
|
|
42
|
+
const withSerwist = withSerwistInit({
|
|
43
|
+
swSrc: 'src/app/sw.ts',
|
|
44
|
+
swDest: 'public/sw.js',
|
|
23
45
|
disable: process.env.NODE_ENV === 'development'
|
|
24
46
|
});
|
|
25
47
|
|
|
@@ -36,4 +58,4 @@ const sentryConfig = {
|
|
|
36
58
|
|
|
37
59
|
const enhancedConfig = withPzConfig(nextConfig);
|
|
38
60
|
|
|
39
|
-
export default withSentryConfig(
|
|
61
|
+
export default withSentryConfig(withSerwist(enhancedConfig), sentryConfig);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectzeronext",
|
|
3
|
-
"version": "2.0.6-rc.
|
|
3
|
+
"version": "2.0.6-rc.2",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"scripts": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"build": "next build --webpack",
|
|
11
11
|
"start": "next start -p 8008",
|
|
12
12
|
"type-check": "tsc",
|
|
13
|
-
"lint": "
|
|
13
|
+
"lint": "eslint .",
|
|
14
14
|
"test": "jest",
|
|
15
15
|
"cypress:headless": "cd projectzeroe2e && yarn headless",
|
|
16
16
|
"e2e:headless": "CYPRESS_BASE_URL=http://localhost:3000 start-server-and-test start http://localhost:3000 cypress:headless",
|
|
@@ -24,42 +24,43 @@
|
|
|
24
24
|
"test:middleware": "jest middleware-matcher.test.ts --bail"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@akinon/next": "2.0.6-rc.
|
|
28
|
-
"@akinon/pz-akifast": "2.0.6-rc.
|
|
29
|
-
"@akinon/pz-apple-pay": "2.0.6-rc.
|
|
30
|
-
"@akinon/pz-b2b": "2.0.6-rc.
|
|
31
|
-
"@akinon/pz-basket-gift-pack": "2.0.6-rc.
|
|
32
|
-
"@akinon/pz-bkm": "2.0.6-rc.
|
|
33
|
-
"@akinon/pz-checkout-gift-pack": "2.0.6-rc.
|
|
34
|
-
"@akinon/pz-click-collect": "2.0.6-rc.
|
|
35
|
-
"@akinon/pz-credit-payment": "2.0.6-rc.
|
|
36
|
-
"@akinon/pz-cybersource-uc": "2.0.6-rc.
|
|
37
|
-
"@akinon/pz-flow-payment": "2.0.6-rc.
|
|
38
|
-
"@akinon/pz-google-pay": "2.0.6-rc.
|
|
39
|
-
"@akinon/pz-gpay": "2.0.6-rc.
|
|
40
|
-
"@akinon/pz-haso": "2.0.6-rc.
|
|
41
|
-
"@akinon/pz-hepsipay": "2.0.6-rc.
|
|
42
|
-
"@akinon/pz-masterpass": "2.0.6-rc.
|
|
43
|
-
"@akinon/pz-masterpass-rest": "2.0.6-rc.
|
|
44
|
-
"@akinon/pz-multi-basket": "2.0.6-rc.
|
|
45
|
-
"@akinon/pz-one-click-checkout": "2.0.6-rc.
|
|
46
|
-
"@akinon/pz-otp": "2.0.6-rc.
|
|
47
|
-
"@akinon/pz-pay-on-delivery": "2.0.6-rc.
|
|
48
|
-
"@akinon/pz-saved-card": "2.0.6-rc.
|
|
49
|
-
"@akinon/pz-similar-products": "2.0.6-rc.
|
|
50
|
-
"@akinon/pz-tabby-extension": "2.0.6-rc.
|
|
51
|
-
"@akinon/pz-tamara-extension": "2.0.6-rc.
|
|
52
|
-
"@akinon/pz-theme": "2.0.6-rc.
|
|
53
|
-
"@akinon/pz-virtual-try-on": "2.0.6-rc.
|
|
27
|
+
"@akinon/next": "2.0.6-rc.2",
|
|
28
|
+
"@akinon/pz-akifast": "2.0.6-rc.2",
|
|
29
|
+
"@akinon/pz-apple-pay": "2.0.6-rc.2",
|
|
30
|
+
"@akinon/pz-b2b": "2.0.6-rc.2",
|
|
31
|
+
"@akinon/pz-basket-gift-pack": "2.0.6-rc.2",
|
|
32
|
+
"@akinon/pz-bkm": "2.0.6-rc.2",
|
|
33
|
+
"@akinon/pz-checkout-gift-pack": "2.0.6-rc.2",
|
|
34
|
+
"@akinon/pz-click-collect": "2.0.6-rc.2",
|
|
35
|
+
"@akinon/pz-credit-payment": "2.0.6-rc.2",
|
|
36
|
+
"@akinon/pz-cybersource-uc": "2.0.6-rc.2",
|
|
37
|
+
"@akinon/pz-flow-payment": "2.0.6-rc.2",
|
|
38
|
+
"@akinon/pz-google-pay": "2.0.6-rc.2",
|
|
39
|
+
"@akinon/pz-gpay": "2.0.6-rc.2",
|
|
40
|
+
"@akinon/pz-haso": "2.0.6-rc.2",
|
|
41
|
+
"@akinon/pz-hepsipay": "2.0.6-rc.2",
|
|
42
|
+
"@akinon/pz-masterpass": "2.0.6-rc.2",
|
|
43
|
+
"@akinon/pz-masterpass-rest": "2.0.6-rc.2",
|
|
44
|
+
"@akinon/pz-multi-basket": "2.0.6-rc.2",
|
|
45
|
+
"@akinon/pz-one-click-checkout": "2.0.6-rc.2",
|
|
46
|
+
"@akinon/pz-otp": "2.0.6-rc.2",
|
|
47
|
+
"@akinon/pz-pay-on-delivery": "2.0.6-rc.2",
|
|
48
|
+
"@akinon/pz-saved-card": "2.0.6-rc.2",
|
|
49
|
+
"@akinon/pz-similar-products": "2.0.6-rc.2",
|
|
50
|
+
"@akinon/pz-tabby-extension": "2.0.6-rc.2",
|
|
51
|
+
"@akinon/pz-tamara-extension": "2.0.6-rc.2",
|
|
52
|
+
"@akinon/pz-theme": "2.0.6-rc.2",
|
|
53
|
+
"@akinon/pz-virtual-try-on": "2.0.6-rc.2",
|
|
54
54
|
"@hookform/resolvers": "2.9.0",
|
|
55
55
|
"@next/third-parties": "16.2.4",
|
|
56
56
|
"@react-google-maps/api": "2.17.1",
|
|
57
57
|
"dayjs": "1.11.5",
|
|
58
58
|
"lossless-json": "2.0.5",
|
|
59
59
|
"next": "16.2.4",
|
|
60
|
+
"@serwist/next": "9.5.11",
|
|
60
61
|
"next-auth": "5.0.0-beta.30",
|
|
61
|
-
"next-pwa": "5.6.0",
|
|
62
62
|
"pino": "8.21.0",
|
|
63
|
+
"serwist": "9.5.11",
|
|
63
64
|
"postcss": "8.4.49",
|
|
64
65
|
"react": "19.2.5",
|
|
65
66
|
"react-dom": "19.2.5",
|
|
@@ -73,7 +74,7 @@
|
|
|
73
74
|
"yup": "0.32.11"
|
|
74
75
|
},
|
|
75
76
|
"devDependencies": {
|
|
76
|
-
"@akinon/eslint-plugin-projectzero": "2.0.6-rc.
|
|
77
|
+
"@akinon/eslint-plugin-projectzero": "2.0.6-rc.2",
|
|
77
78
|
"@semantic-release/changelog": "6.0.2",
|
|
78
79
|
"@semantic-release/exec": "6.0.3",
|
|
79
80
|
"@semantic-release/git": "10.0.1",
|
|
@@ -87,14 +88,12 @@
|
|
|
87
88
|
"@types/jest": "29.5.14",
|
|
88
89
|
"@types/react": "19.2.14",
|
|
89
90
|
"@types/react-dom": "19.2.3",
|
|
90
|
-
"@typescript-eslint/eslint-plugin": "6.7.4",
|
|
91
|
-
"@typescript-eslint/parser": "6.7.4",
|
|
92
91
|
"client-only": "0.0.1",
|
|
93
92
|
"clsx": "1.2.1",
|
|
94
93
|
"currency-symbol-map": "5.1.0",
|
|
95
|
-
"eslint": "
|
|
94
|
+
"eslint": "9.39.4",
|
|
96
95
|
"eslint-config-next": "16.2.4",
|
|
97
|
-
"eslint-config-prettier": "
|
|
96
|
+
"eslint-config-prettier": "10.1.1",
|
|
98
97
|
"husky": "8.0.0",
|
|
99
98
|
"jest": "29.7.0",
|
|
100
99
|
"jest-css-modules-transform": "4.4.2",
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"question": "Are you sure you want to delete the product?",
|
|
21
21
|
"delete": "Delete",
|
|
22
22
|
"add_to_favorites": "Delete and Add to Favorites",
|
|
23
|
-
"cancel": "Cancel"
|
|
23
|
+
"cancel": "Cancel",
|
|
24
|
+
"moved_to_favorites": "Moved to favorites",
|
|
25
|
+
"removed": "Removed from basket",
|
|
26
|
+
"error": "Failed to remove product"
|
|
24
27
|
}
|
|
25
28
|
},
|
|
26
29
|
"summary": {
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
+
"network": {
|
|
3
|
+
"offline": {
|
|
4
|
+
"title": "Connection Lost",
|
|
5
|
+
"message": "Your internet connection has been lost. Please check your connection and try again."
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
"install": {
|
|
9
|
+
"title": "Install App",
|
|
10
|
+
"description": {
|
|
11
|
+
"default": "Add this site to your home screen for faster access.",
|
|
12
|
+
"ios": "To add this site to your home screen, tap the share button in Safari and select 'Add to Home Screen'."
|
|
13
|
+
},
|
|
14
|
+
"button": "Install",
|
|
15
|
+
"dismiss": "Close"
|
|
16
|
+
},
|
|
2
17
|
"header": {
|
|
3
18
|
"login": "LOGIN",
|
|
4
19
|
"signup": "SIGNUP"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "Reset Password",
|
|
3
3
|
"description": "Enter your email address and we'll send you a link to reset your password.",
|
|
4
|
+
"description_first": "Enter your email address,",
|
|
5
|
+
"description_second": "we'll send you a link to reset your password.",
|
|
4
6
|
"back_to_login": "Back to login",
|
|
5
7
|
"form": {
|
|
6
8
|
"email": {
|
|
@@ -13,7 +15,8 @@
|
|
|
13
15
|
},
|
|
14
16
|
"success": {
|
|
15
17
|
"title": "Reset Password",
|
|
16
|
-
"subtitle": "We've sent a password reset link to {EMAIL}. Please check your inbox and follow the instructions."
|
|
18
|
+
"subtitle": "We've sent a password reset link to {EMAIL}. Please check your inbox and follow the instructions.",
|
|
19
|
+
"button": "Continue to Homepage"
|
|
17
20
|
}
|
|
18
21
|
},
|
|
19
22
|
"create_new_password": {
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
"add_to_cart": "ADD TO CART",
|
|
3
3
|
"add_stock_alert": "ADD STOCK ALERT",
|
|
4
4
|
"add_to_favorites": "Add to Favorites",
|
|
5
|
+
"added_to_favorites": "Added to favorites",
|
|
6
|
+
"removed_from_favorites": "Removed from favorites",
|
|
7
|
+
"favorites_error": "Failed to update favorites",
|
|
5
8
|
"share": "Share",
|
|
6
9
|
"details_care": "Details & Care",
|
|
7
10
|
"delivery_collections": "Delivery, Collections & Returns",
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"question": "Ürünü silmek istediğinizden emin misiniz?",
|
|
21
21
|
"delete": "Sil",
|
|
22
22
|
"add_to_favorites": "Sil ve Favorilere Ekle",
|
|
23
|
-
"cancel": "Vazgeç"
|
|
23
|
+
"cancel": "Vazgeç",
|
|
24
|
+
"moved_to_favorites": "Favorilere taşındı",
|
|
25
|
+
"removed": "Sepetten kaldırıldı",
|
|
26
|
+
"error": "Ürün kaldırılamadı"
|
|
24
27
|
}
|
|
25
28
|
},
|
|
26
29
|
"summary": {
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
+
"network": {
|
|
3
|
+
"offline": {
|
|
4
|
+
"title": "Bağlantı Kesildi",
|
|
5
|
+
"message": "İnternet bağlantınız kesildi. Lütfen bağlantınızı kontrol edip tekrar deneyin."
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
"install": {
|
|
9
|
+
"title": "Uygulamayı Yükle",
|
|
10
|
+
"description": {
|
|
11
|
+
"default": "Daha hızlı erişim için bu siteyi ana ekranına ekle.",
|
|
12
|
+
"ios": "Ana ekranına eklemek için Safari'nin paylaş butonuna basıp 'Ana Ekrana Ekle' seçeneğini seç."
|
|
13
|
+
},
|
|
14
|
+
"button": "Yükle",
|
|
15
|
+
"dismiss": "Kapat"
|
|
16
|
+
},
|
|
2
17
|
"header": {
|
|
3
18
|
"login": "GİRİŞ YAP",
|
|
4
19
|
"signup": "ÜYE OL"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "Şifre Sıfırlama",
|
|
3
3
|
"description": "E-posta adresinizi girin, şifrenizi sıfırlamanız için size bir bağlantı gönderelim.",
|
|
4
|
+
"description_first": "E-posta adresinizi girin,",
|
|
5
|
+
"description_second": "şifrenizi sıfırlamanız için size bir bağlantı gönderelim.",
|
|
4
6
|
"back_to_login": "Girişe dön",
|
|
5
7
|
"form": {
|
|
6
8
|
"email": {
|
|
@@ -13,7 +15,8 @@
|
|
|
13
15
|
},
|
|
14
16
|
"success": {
|
|
15
17
|
"title": "Şifre Sıfırlama",
|
|
16
|
-
"subtitle": "{EMAIL} adresine bir şifre sıfırlama bağlantısı gönderdik. Lütfen gelen kutunuzu kontrol edin ve talimatları izleyin."
|
|
18
|
+
"subtitle": "{EMAIL} adresine bir şifre sıfırlama bağlantısı gönderdik. Lütfen gelen kutunuzu kontrol edin ve talimatları izleyin.",
|
|
19
|
+
"button": "Ana Sayfaya Devam Et"
|
|
17
20
|
}
|
|
18
21
|
},
|
|
19
22
|
"create_new_password": {
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
"add_to_cart": "SEPETE EKLE",
|
|
3
3
|
"add_stock_alert": "STOK UYARISI EKLE",
|
|
4
4
|
"add_to_favorites": "Favorilere ekle",
|
|
5
|
+
"added_to_favorites": "Favorilere eklendi",
|
|
6
|
+
"removed_from_favorites": "Favorilerden kaldırıldı",
|
|
7
|
+
"favorites_error": "Favoriler güncellenemedi",
|
|
5
8
|
"share": "Paylaş",
|
|
6
9
|
"details_care": "DETAYLAR & BAKIM",
|
|
7
10
|
"delivery_collections": "TESLİMAT, KOLEKSİYON VE İADE",
|
|
@@ -1,8 +1,64 @@
|
|
|
1
|
-
import { getFlatPageData } from '@akinon/next/data/server';
|
|
1
|
+
import { getFlatPageData, getSeoData } from '@akinon/next/data/server';
|
|
2
2
|
import { withSegmentDefaults } from '@akinon/next/hocs/server';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
AsyncPageProps,
|
|
5
|
+
Metadata,
|
|
6
|
+
ResolvedPageProps
|
|
7
|
+
} from '@akinon/next/types';
|
|
8
|
+
import { parsePzParams } from '@akinon/next/utils/pz-segments';
|
|
9
|
+
import logger from '@akinon/next/utils/log';
|
|
10
|
+
import settings from '@theme/settings';
|
|
4
11
|
import { notFound } from 'next/navigation';
|
|
5
12
|
|
|
13
|
+
export async function generateMetadata(
|
|
14
|
+
props: AsyncPageProps<{ pk: number }>
|
|
15
|
+
): Promise<Metadata> {
|
|
16
|
+
const params = await props.params;
|
|
17
|
+
const { locale } = parsePzParams(params, settings);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const data = await getFlatPageData({ pk: params.pk, locale });
|
|
21
|
+
const flatPage = data?.flat_page;
|
|
22
|
+
|
|
23
|
+
if (!flatPage) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const localizedUrl = flatPage.flatpageprettyurl_set?.find(
|
|
28
|
+
(entry) => entry.language === locale
|
|
29
|
+
)?.url;
|
|
30
|
+
const seoUrl = localizedUrl ?? flatPage.url;
|
|
31
|
+
|
|
32
|
+
if (!seoUrl) {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const seo = await getSeoData(seoUrl, locale);
|
|
37
|
+
|
|
38
|
+
if (!seo?.title && !seo?.description && !seo?.keywords) {
|
|
39
|
+
logger.debug('No CMS SEO entry for flat-page', {
|
|
40
|
+
pk: params.pk,
|
|
41
|
+
seoUrl,
|
|
42
|
+
locale
|
|
43
|
+
});
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...(seo.title ? { title: seo.title } : {}),
|
|
49
|
+
...(seo.description ? { description: seo.description } : {}),
|
|
50
|
+
...(seo.keywords ? { keywords: seo.keywords } : {})
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.warn('flat-page generateMetadata failed', {
|
|
54
|
+
pk: params.pk,
|
|
55
|
+
locale,
|
|
56
|
+
error
|
|
57
|
+
});
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
6
62
|
async function Page({ params }: ResolvedPageProps<{ pk: number }>) {
|
|
7
63
|
try {
|
|
8
64
|
const data = await getFlatPageData({ pk: params.pk });
|
|
@@ -3,7 +3,7 @@ import { ProductGroupInfo } from '@theme/views/product';
|
|
|
3
3
|
import { getProductData, getWidgetData } from '@akinon/next/data/server';
|
|
4
4
|
import { withSegmentDefaults } from '@akinon/next/hocs/server';
|
|
5
5
|
import { AsyncPageProps, ResolvedPageProps, Metadata } from '@akinon/next/types';
|
|
6
|
-
import { generateJsonLd } from '@theme/utils/generate-jsonld';
|
|
6
|
+
import { generateJsonLd, serializeJsonLd } from '@theme/utils/generate-jsonld';
|
|
7
7
|
import { notFound } from 'next/navigation';
|
|
8
8
|
|
|
9
9
|
export async function generateMetadata(
|
|
@@ -78,7 +78,7 @@ async function Page({ params, searchParams }: ResolvedPageProps<{ pk: number }>)
|
|
|
78
78
|
</ProductLayout>
|
|
79
79
|
<script
|
|
80
80
|
type="application/ld+json"
|
|
81
|
-
dangerouslySetInnerHTML={{ __html:
|
|
81
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLd) }}
|
|
82
82
|
/>
|
|
83
83
|
</>
|
|
84
84
|
);
|
|
@@ -11,6 +11,8 @@ import { getSeoData } from '@akinon/next/data/server';
|
|
|
11
11
|
import PzRoot from '@akinon/next/components/pz-root';
|
|
12
12
|
import MobileAppToggler from '@akinon/next/components/mobile-app-toggler';
|
|
13
13
|
import pwaTags from '@theme/components/pwa-tags';
|
|
14
|
+
import { OfflineFallback } from '@theme/components/offline-fallback';
|
|
15
|
+
import { InstallPrompt } from '@theme/components/install-prompt';
|
|
14
16
|
import type { Viewport } from 'next';
|
|
15
17
|
import settings from '@theme/settings';
|
|
16
18
|
import RouteHandler from '@theme/components/route-handler';
|
|
@@ -43,7 +45,11 @@ export async function generateMetadata() {
|
|
|
43
45
|
return result;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
async function RootLayout({
|
|
48
|
+
async function RootLayout({
|
|
49
|
+
locale,
|
|
50
|
+
translations,
|
|
51
|
+
children
|
|
52
|
+
}: ResolvedRootLayoutProps) {
|
|
47
53
|
return (
|
|
48
54
|
<html lang={locale.isoCode} {...(locale.rtl ? { dir: 'rtl' } : {})}>
|
|
49
55
|
<head />
|
|
@@ -55,13 +61,16 @@ async function RootLayout({ locale, translations, children }: ResolvedRootLayout
|
|
|
55
61
|
<Header />
|
|
56
62
|
</MobileAppToggler>
|
|
57
63
|
<main>
|
|
58
|
-
{children}
|
|
64
|
+
<OfflineFallback>{children}</OfflineFallback>
|
|
59
65
|
<RouteHandler />
|
|
60
66
|
<RootModal />
|
|
61
67
|
</main>
|
|
62
68
|
<MobileAppToggler>
|
|
63
69
|
<Footer />
|
|
64
70
|
</MobileAppToggler>
|
|
71
|
+
<MobileAppToggler>
|
|
72
|
+
<InstallPrompt />
|
|
73
|
+
</MobileAppToggler>
|
|
65
74
|
</div>
|
|
66
75
|
</ClientRoot>
|
|
67
76
|
</PzRoot>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getProductData, getWidgetData } from '@akinon/next/data/server';
|
|
2
2
|
import { withSegmentDefaults } from '@akinon/next/hocs/server';
|
|
3
3
|
import { AsyncPageProps, ResolvedPageProps, Metadata } from '@akinon/next/types';
|
|
4
|
-
import { generateJsonLd } from '@theme/utils/generate-jsonld';
|
|
4
|
+
import { generateJsonLd, serializeJsonLd } from '@theme/utils/generate-jsonld';
|
|
5
5
|
import { AccordionWrapper, ProductInfo } from '@theme/views/product';
|
|
6
6
|
import ProductLayout from '@theme/views/product/layout';
|
|
7
7
|
import { notFound } from 'next/navigation';
|
|
@@ -87,7 +87,7 @@ async function Page({ params, searchParams }: ResolvedPageProps<{ pk: number }>)
|
|
|
87
87
|
</ProductLayout>
|
|
88
88
|
<script
|
|
89
89
|
type="application/ld+json"
|
|
90
|
-
dangerouslySetInnerHTML={{ __html:
|
|
90
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLd) }}
|
|
91
91
|
/>
|
|
92
92
|
</>
|
|
93
93
|
);
|
|
@@ -8,6 +8,7 @@ import { closeMiniBasket } from '@akinon/next/redux/reducers/root';
|
|
|
8
8
|
import { ROUTES } from '@theme/routes';
|
|
9
9
|
import { GoogleTagManager } from '@next/third-parties/google';
|
|
10
10
|
import { pushPageView } from '@theme/utils/gtm';
|
|
11
|
+
import { serializeJsonLd } from '@theme/utils/generate-jsonld';
|
|
11
12
|
|
|
12
13
|
export default function RootTemplate({
|
|
13
14
|
children
|
|
@@ -63,11 +64,11 @@ export default function RootTemplate({
|
|
|
63
64
|
{/* End Google Tag Manager */}
|
|
64
65
|
<script
|
|
65
66
|
type="application/ld+json"
|
|
66
|
-
dangerouslySetInnerHTML={{ __html:
|
|
67
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLdWebSite) }}
|
|
67
68
|
/>
|
|
68
69
|
<script
|
|
69
70
|
type="application/ld+json"
|
|
70
|
-
dangerouslySetInnerHTML={{ __html:
|
|
71
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLdOrganization) }}
|
|
71
72
|
/>
|
|
72
73
|
{children}
|
|
73
74
|
</>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MetadataRoute } from 'next'
|
|
2
|
+
|
|
3
|
+
export default function manifest(): MetadataRoute.Manifest {
|
|
4
|
+
return {
|
|
5
|
+
name: 'Project Zero | Next',
|
|
6
|
+
short_name: 'PZ | Next',
|
|
7
|
+
icons: [
|
|
8
|
+
{
|
|
9
|
+
src: '/icon-192x192.png',
|
|
10
|
+
sizes: '192x192',
|
|
11
|
+
type: 'image/png',
|
|
12
|
+
purpose: 'maskable'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
src: '/icon-256x256.png',
|
|
16
|
+
sizes: '256x256',
|
|
17
|
+
type: 'image/png',
|
|
18
|
+
purpose: 'any'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
src: '/icon-384x384.png',
|
|
22
|
+
sizes: '384x384',
|
|
23
|
+
type: 'image/png',
|
|
24
|
+
purpose: 'any'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
src: '/icon-512x512.png',
|
|
28
|
+
sizes: '512x512',
|
|
29
|
+
type: 'image/png',
|
|
30
|
+
purpose: 'any'
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
theme_color: '#FFFFFF',
|
|
34
|
+
background_color: '#FFFFFF',
|
|
35
|
+
start_url: '/',
|
|
36
|
+
display: 'standalone',
|
|
37
|
+
orientation: 'portrait'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/// <reference no-default-lib="true" />
|
|
2
|
+
/// <reference lib="esnext" />
|
|
3
|
+
/// <reference lib="webworker" />
|
|
4
|
+
|
|
5
|
+
import { defaultCache } from '@serwist/next/worker'
|
|
6
|
+
import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist'
|
|
7
|
+
import { Serwist } from 'serwist'
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
interface WorkerGlobalScope extends SerwistGlobalConfig {
|
|
11
|
+
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
declare const self: ServiceWorkerGlobalScope
|
|
16
|
+
|
|
17
|
+
const serwist = new Serwist({
|
|
18
|
+
precacheEntries: self.__SW_MANIFEST,
|
|
19
|
+
skipWaiting: true,
|
|
20
|
+
clientsClaim: true,
|
|
21
|
+
navigationPreload: true,
|
|
22
|
+
runtimeCaching: defaultCache
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
serwist.addEventListeners()
|
|
@@ -40,5 +40,7 @@ export * from './skeleton-wrapper';
|
|
|
40
40
|
// Head
|
|
41
41
|
export * from './canonical-url';
|
|
42
42
|
export * from './pwa-tags';
|
|
43
|
+
export * from './offline-fallback';
|
|
44
|
+
export * from './install-prompt';
|
|
43
45
|
export * from './quantity-input';
|
|
44
46
|
export * from './quantity-selector';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
import { Button } from '@theme/components';
|
|
6
|
+
import { useInstallPrompt } from '@theme/hooks/use-install-prompt';
|
|
7
|
+
import { pushEventGA4 } from '@theme/utils/gtm';
|
|
8
|
+
|
|
9
|
+
const trackEvent = (event: string, params: Record<string, unknown>) => {
|
|
10
|
+
try {
|
|
11
|
+
pushEventGA4(event, params);
|
|
12
|
+
} catch {
|
|
13
|
+
// dataLayer unavailable (GTM blocked, script not loaded yet)
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const InstallPrompt = () => {
|
|
18
|
+
const { canInstall, isIOS, install, dismiss } = useInstallPrompt();
|
|
19
|
+
const { t } = useLocalization();
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!canInstall) return;
|
|
23
|
+
trackEvent('pwa_install_prompt_shown', {
|
|
24
|
+
platform: isIOS ? 'ios' : 'native'
|
|
25
|
+
});
|
|
26
|
+
}, [canInstall, isIOS]);
|
|
27
|
+
|
|
28
|
+
if (!canInstall) return null;
|
|
29
|
+
|
|
30
|
+
const handleInstall = async () => {
|
|
31
|
+
trackEvent('pwa_install_prompt_clicked', { platform: 'native' });
|
|
32
|
+
const outcome = await install();
|
|
33
|
+
if (outcome) {
|
|
34
|
+
trackEvent('pwa_install_prompt_outcome', { outcome });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const handleDismiss = () => {
|
|
39
|
+
trackEvent('pwa_install_prompt_dismissed', {
|
|
40
|
+
platform: isIOS ? 'ios' : 'native'
|
|
41
|
+
});
|
|
42
|
+
dismiss();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
role="region"
|
|
48
|
+
aria-label={t('common.install.title')}
|
|
49
|
+
className="fixed bottom-0 left-0 right-0 z-40 border-t border-gray-200 bg-white p-4 shadow-lg"
|
|
50
|
+
>
|
|
51
|
+
<div className="mx-auto flex max-w-screen-lg items-start gap-4">
|
|
52
|
+
<div className="flex-1 min-w-0">
|
|
53
|
+
<h3 className="font-semibold mb-1">
|
|
54
|
+
{t('common.install.title')}
|
|
55
|
+
</h3>
|
|
56
|
+
<p className="text-sm text-gray-700">
|
|
57
|
+
{isIOS
|
|
58
|
+
? t('common.install.description.ios')
|
|
59
|
+
: t('common.install.description.default')}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
63
|
+
{!isIOS && (
|
|
64
|
+
<Button onClick={handleInstall} size="sm">
|
|
65
|
+
{t('common.install.button')}
|
|
66
|
+
</Button>
|
|
67
|
+
)}
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={handleDismiss}
|
|
71
|
+
aria-label={t('common.install.dismiss')}
|
|
72
|
+
className="p-2 text-gray-500 hover:text-gray-700 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
|
73
|
+
>
|
|
74
|
+
<svg
|
|
75
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
76
|
+
viewBox="0 0 24 24"
|
|
77
|
+
fill="none"
|
|
78
|
+
stroke="currentColor"
|
|
79
|
+
strokeWidth={2}
|
|
80
|
+
strokeLinecap="round"
|
|
81
|
+
strokeLinejoin="round"
|
|
82
|
+
className="size-5"
|
|
83
|
+
aria-hidden="true"
|
|
84
|
+
>
|
|
85
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
86
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
87
|
+
</svg>
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
5
|
+
import { useOnlineStatus } from '@theme/hooks/use-online-status';
|
|
6
|
+
|
|
7
|
+
interface OfflineFallbackProps {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const OfflineFallback = ({ children }: OfflineFallbackProps) => {
|
|
12
|
+
const isOnline = useOnlineStatus();
|
|
13
|
+
const { t } = useLocalization();
|
|
14
|
+
|
|
15
|
+
if (isOnline) return <>{children}</>;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
role="status"
|
|
20
|
+
aria-live="polite"
|
|
21
|
+
className="flex flex-col items-center justify-center min-h-[60vh] px-4 py-16 text-center"
|
|
22
|
+
>
|
|
23
|
+
<svg
|
|
24
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
25
|
+
viewBox="0 0 24 24"
|
|
26
|
+
fill="none"
|
|
27
|
+
stroke="currentColor"
|
|
28
|
+
strokeWidth={1.5}
|
|
29
|
+
strokeLinecap="round"
|
|
30
|
+
strokeLinejoin="round"
|
|
31
|
+
className="w-16 h-16 mb-6 text-gray-400"
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
>
|
|
34
|
+
<line x1="1" y1="1" x2="23" y2="23" />
|
|
35
|
+
<path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55" />
|
|
36
|
+
<path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39" />
|
|
37
|
+
<path d="M10.71 5.05A16 16 0 0 1 22.58 9" />
|
|
38
|
+
<path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88" />
|
|
39
|
+
<path d="M8.53 16.11a6 6 0 0 1 6.95 0" />
|
|
40
|
+
<line x1="12" y1="20" x2="12.01" y2="20" />
|
|
41
|
+
</svg>
|
|
42
|
+
<h2 className="text-2xl font-semibold mb-3">
|
|
43
|
+
{t('common.network.offline.title')}
|
|
44
|
+
</h2>
|
|
45
|
+
<p className="text-gray-600 max-w-md">
|
|
46
|
+
{t('common.network.offline.message')}
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '@akinon/next/data/client/wishlist';
|
|
11
11
|
import { Icon } from '@theme/components';
|
|
12
12
|
import { pushAddToWishlist } from '@theme/utils/gtm';
|
|
13
|
+
import { useToast, useLocalization } from '@akinon/next/hooks';
|
|
13
14
|
|
|
14
15
|
interface FavButtonProps {
|
|
15
16
|
label?: string;
|
|
@@ -18,8 +19,12 @@ interface FavButtonProps {
|
|
|
18
19
|
style?: React.CSSProperties;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
const FAV_TOAST_OPTIONS = { position: 'bottom-right' as const }
|
|
23
|
+
|
|
21
24
|
const useFavButton = (productPk: number) => {
|
|
22
25
|
const { status } = useSession();
|
|
26
|
+
const { t } = useLocalization();
|
|
27
|
+
const { success, error: showError } = useToast();
|
|
23
28
|
|
|
24
29
|
const { data: favorites } = useGetFavoritesQuery(
|
|
25
30
|
{
|
|
@@ -45,15 +50,17 @@ const useFavButton = (productPk: number) => {
|
|
|
45
50
|
const handleClick = useCallback(async () => {
|
|
46
51
|
try {
|
|
47
52
|
if (favoriteItem) {
|
|
48
|
-
await removeFavorite(favoriteItem.pk);
|
|
53
|
+
await removeFavorite(favoriteItem.pk).unwrap();
|
|
54
|
+
success(t('product.removed_from_favorites'), FAV_TOAST_OPTIONS);
|
|
49
55
|
} else {
|
|
50
|
-
await addFavorite(productPk);
|
|
56
|
+
await addFavorite(productPk).unwrap();
|
|
51
57
|
setIsPushed(true);
|
|
58
|
+
success(t('product.added_to_favorites'), FAV_TOAST_OPTIONS);
|
|
52
59
|
}
|
|
53
|
-
} catch (
|
|
54
|
-
|
|
60
|
+
} catch (err) {
|
|
61
|
+
showError(t('product.favorites_error'), FAV_TOAST_OPTIONS);
|
|
55
62
|
}
|
|
56
|
-
}, [favoriteItem, productPk, addFavorite, removeFavorite]);
|
|
63
|
+
}, [favoriteItem, productPk, addFavorite, removeFavorite, success, showError, t]);
|
|
57
64
|
|
|
58
65
|
useEffect(() => {
|
|
59
66
|
if (favoriteItem && isPushed) {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { usePathname } from 'next/navigation';
|
|
4
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
interface BeforeInstallPromptEvent extends Event {
|
|
7
|
+
prompt: () => Promise<void>;
|
|
8
|
+
userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DISMISS_STORAGE_KEY = 'pz-install-prompt-dismissed-at';
|
|
12
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
const CONVERSION_PATH_PATTERNS = ['/basket', '/orders/'];
|
|
15
|
+
|
|
16
|
+
const IN_APP_BROWSER_PATTERN =
|
|
17
|
+
/FBAN|FBAV|FB_IAB|Instagram|Twitter|Line\/|LinkedInApp|TikTok|Snapchat|; wv\)/i;
|
|
18
|
+
const NON_SAFARI_IOS_PATTERN = /CriOS|FxiOS|EdgiOS|OPiOS|YaBrowser|UCBrowser/i;
|
|
19
|
+
|
|
20
|
+
const safeRead = (key: string): string | null => {
|
|
21
|
+
try {
|
|
22
|
+
return window.localStorage.getItem(key);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const safeWrite = (key: string, value: string): void => {
|
|
29
|
+
try {
|
|
30
|
+
window.localStorage.setItem(key, value);
|
|
31
|
+
} catch {
|
|
32
|
+
// Safari Private Mode, quota exceeded — fail soft
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const isWithinDismissWindow = (timestamp: string | null): boolean => {
|
|
37
|
+
if (!timestamp) return false;
|
|
38
|
+
const dismissedAt = Number(timestamp);
|
|
39
|
+
if (!Number.isFinite(dismissedAt)) return false;
|
|
40
|
+
return Date.now() - dismissedAt < SEVEN_DAYS_MS;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const detectIOS = (): boolean => {
|
|
44
|
+
const ua = window.navigator.userAgent;
|
|
45
|
+
const isAppleMobile = /iPad|iPhone|iPod/.test(ua);
|
|
46
|
+
// iPadOS 13+ reports as Macintosh; disambiguate via touch points
|
|
47
|
+
const isIPadOSAsMac = /Mac/.test(ua) && navigator.maxTouchPoints > 1;
|
|
48
|
+
const isMSStream = !!(window as Window & { MSStream?: unknown }).MSStream;
|
|
49
|
+
return (isAppleMobile || isIPadOSAsMac) && !isMSStream;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const detectStandalone = (): boolean =>
|
|
53
|
+
window.matchMedia('(display-mode: standalone)').matches ||
|
|
54
|
+
(window.navigator as Navigator & { standalone?: boolean }).standalone === true;
|
|
55
|
+
|
|
56
|
+
const detectInAppBrowser = (): boolean =>
|
|
57
|
+
IN_APP_BROWSER_PATTERN.test(window.navigator.userAgent);
|
|
58
|
+
|
|
59
|
+
const detectNonSafariIOSBrowser = (): boolean =>
|
|
60
|
+
NON_SAFARI_IOS_PATTERN.test(window.navigator.userAgent);
|
|
61
|
+
|
|
62
|
+
const isConversionPath = (pathname: string): boolean =>
|
|
63
|
+
CONVERSION_PATH_PATTERNS.some((pattern) => pathname.includes(pattern));
|
|
64
|
+
|
|
65
|
+
export const useInstallPrompt = () => {
|
|
66
|
+
const pathname = usePathname() ?? '';
|
|
67
|
+
|
|
68
|
+
const [isHydrated, setIsHydrated] = useState(false);
|
|
69
|
+
const [deferredPrompt, setDeferredPrompt] =
|
|
70
|
+
useState<BeforeInstallPromptEvent | null>(null);
|
|
71
|
+
const [isIOS, setIsIOS] = useState(false);
|
|
72
|
+
const [isStandalone, setIsStandalone] = useState(false);
|
|
73
|
+
const [isInAppBrowser, setIsInAppBrowser] = useState(false);
|
|
74
|
+
const [isNonSafariIOS, setIsNonSafariIOS] = useState(false);
|
|
75
|
+
const [isDismissed, setIsDismissed] = useState(false);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
setIsStandalone(detectStandalone());
|
|
79
|
+
setIsIOS(detectIOS());
|
|
80
|
+
setIsInAppBrowser(detectInAppBrowser());
|
|
81
|
+
setIsNonSafariIOS(detectNonSafariIOSBrowser());
|
|
82
|
+
setIsDismissed(isWithinDismissWindow(safeRead(DISMISS_STORAGE_KEY)));
|
|
83
|
+
setIsHydrated(true);
|
|
84
|
+
|
|
85
|
+
const handleBeforeInstallPrompt = (event: Event) => {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
setDeferredPrompt(event as BeforeInstallPromptEvent);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleAppInstalled = () => {
|
|
91
|
+
setDeferredPrompt(null);
|
|
92
|
+
setIsStandalone(true);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handleStorageChange = (event: StorageEvent) => {
|
|
96
|
+
if (event.key !== DISMISS_STORAGE_KEY) return;
|
|
97
|
+
setIsDismissed(isWithinDismissWindow(event.newValue));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
|
101
|
+
window.addEventListener('appinstalled', handleAppInstalled);
|
|
102
|
+
window.addEventListener('storage', handleStorageChange);
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
window.removeEventListener(
|
|
106
|
+
'beforeinstallprompt',
|
|
107
|
+
handleBeforeInstallPrompt
|
|
108
|
+
);
|
|
109
|
+
window.removeEventListener('appinstalled', handleAppInstalled);
|
|
110
|
+
window.removeEventListener('storage', handleStorageChange);
|
|
111
|
+
};
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
const dismiss = useCallback(() => {
|
|
115
|
+
safeWrite(DISMISS_STORAGE_KEY, Date.now().toString());
|
|
116
|
+
setIsDismissed(true);
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
const install = useCallback(async () => {
|
|
120
|
+
if (!deferredPrompt) return null;
|
|
121
|
+
await deferredPrompt.prompt();
|
|
122
|
+
const { outcome } = await deferredPrompt.userChoice;
|
|
123
|
+
if (outcome === 'accepted') {
|
|
124
|
+
setDeferredPrompt(null);
|
|
125
|
+
} else {
|
|
126
|
+
dismiss();
|
|
127
|
+
}
|
|
128
|
+
return outcome;
|
|
129
|
+
}, [deferredPrompt, dismiss]);
|
|
130
|
+
|
|
131
|
+
const canInstall =
|
|
132
|
+
isHydrated &&
|
|
133
|
+
!isStandalone &&
|
|
134
|
+
!isDismissed &&
|
|
135
|
+
!isInAppBrowser &&
|
|
136
|
+
!isConversionPath(pathname) &&
|
|
137
|
+
(deferredPrompt !== null || (isIOS && !isNonSafariIOS));
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
canInstall,
|
|
141
|
+
isIOS,
|
|
142
|
+
isStandalone,
|
|
143
|
+
install,
|
|
144
|
+
dismiss
|
|
145
|
+
};
|
|
146
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
4
|
+
|
|
5
|
+
const subscribe = (callback: () => void) => {
|
|
6
|
+
window.addEventListener('online', callback);
|
|
7
|
+
window.addEventListener('offline', callback);
|
|
8
|
+
return () => {
|
|
9
|
+
window.removeEventListener('online', callback);
|
|
10
|
+
window.removeEventListener('offline', callback);
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const getSnapshot = () => navigator.onLine;
|
|
15
|
+
const getServerSnapshot = () => true;
|
|
16
|
+
|
|
17
|
+
const OFFLINE_DEBOUNCE_MS = 500;
|
|
18
|
+
|
|
19
|
+
export const useOnlineStatus = () => {
|
|
20
|
+
const isOnlineRaw = useSyncExternalStore(
|
|
21
|
+
subscribe,
|
|
22
|
+
getSnapshot,
|
|
23
|
+
getServerSnapshot
|
|
24
|
+
);
|
|
25
|
+
const [isOnline, setIsOnline] = useState(isOnlineRaw);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (isOnlineRaw) {
|
|
29
|
+
setIsOnline(true);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const timer = window.setTimeout(
|
|
33
|
+
() => setIsOnline(false),
|
|
34
|
+
OFFLINE_DEBOUNCE_MS
|
|
35
|
+
);
|
|
36
|
+
return () => window.clearTimeout(timer);
|
|
37
|
+
}, [isOnlineRaw]);
|
|
38
|
+
|
|
39
|
+
return isOnline;
|
|
40
|
+
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Product } from '@akinon/next/types/commerce/product';
|
|
2
2
|
|
|
3
|
+
export const serializeJsonLd = (data: object): string =>
|
|
4
|
+
JSON.stringify(data).replace(/</g, '\\u003c');
|
|
5
|
+
|
|
3
6
|
export const generateJsonLd = (product: Product) => {
|
|
4
7
|
const URL = process.env.NEXT_PUBLIC_URL;
|
|
5
8
|
|
|
@@ -9,7 +9,8 @@ import { useState } from 'react';
|
|
|
9
9
|
import { useAddFavoriteMutation } from '@akinon/next/data/client/wishlist';
|
|
10
10
|
import {
|
|
11
11
|
useCommonProductAttributes,
|
|
12
|
-
useLocalization
|
|
12
|
+
useLocalization,
|
|
13
|
+
useToast
|
|
13
14
|
} from '@akinon/next/hooks';
|
|
14
15
|
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
15
16
|
import { Image } from '@akinon/next/components/image';
|
|
@@ -23,6 +24,7 @@ interface Props {
|
|
|
23
24
|
|
|
24
25
|
export const BasketItem = (props: Props) => {
|
|
25
26
|
const { t } = useLocalization();
|
|
27
|
+
const { success, error: showError } = useToast();
|
|
26
28
|
const { basketItem, namespace } = props;
|
|
27
29
|
const [updateQuantityMutation] = useUpdateQuantityMutation();
|
|
28
30
|
const dispatch = useAppDispatch();
|
|
@@ -78,9 +80,12 @@ export const BasketItem = (props: Props) => {
|
|
|
78
80
|
|
|
79
81
|
if (productPk) {
|
|
80
82
|
await addFavorite(productPk);
|
|
83
|
+
success(t('basket.card.modal.moved_to_favorites'));
|
|
84
|
+
} else {
|
|
85
|
+
success(t('basket.card.modal.removed'));
|
|
81
86
|
}
|
|
82
|
-
} catch (
|
|
83
|
-
|
|
87
|
+
} catch (err) {
|
|
88
|
+
showError(t('basket.card.modal.error'));
|
|
84
89
|
} finally {
|
|
85
90
|
setUpdateQuantityLoading(false);
|
|
86
91
|
setRemoveBasketModalOpen(false);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { Button, Icon, Modal } from '@theme/components';
|
|
4
|
-
import { useLocalization } from '@akinon/next/hooks';
|
|
4
|
+
import { useLocalization, useToast } from '@akinon/next/hooks';
|
|
5
5
|
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
6
|
+
import { formatErrorMessage } from '@akinon/next/utils/format-error-message';
|
|
6
7
|
import { validateVariantSelection } from '../../utils/variant-validation';
|
|
7
8
|
import { VariantType } from '@akinon/next/types';
|
|
8
9
|
|
|
@@ -44,6 +45,7 @@ export const ProductActions: React.FC<ProductActionsProps> = ({
|
|
|
44
45
|
onCloseModal
|
|
45
46
|
}) => {
|
|
46
47
|
const { t } = useLocalization();
|
|
48
|
+
const { error: showError } = useToast();
|
|
47
49
|
|
|
48
50
|
const checkoutProviderProps = {
|
|
49
51
|
product,
|
|
@@ -75,8 +77,7 @@ export const ProductActions: React.FC<ProductActionsProps> = ({
|
|
|
75
77
|
Object.keys(error?.data || {}).map(
|
|
76
78
|
(key) => `${key}: ${error?.data[key].join(', ')}`
|
|
77
79
|
);
|
|
78
|
-
|
|
79
|
-
console.error('Checkout error:', formattedError);
|
|
80
|
+
showError(formatErrorMessage(formattedError));
|
|
80
81
|
}
|
|
81
82
|
};
|
|
82
83
|
|
|
@@ -63,6 +63,26 @@ const defaultConfig = {
|
|
|
63
63
|
transform: 'translateX(100%)'
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
+
'toast-in': {
|
|
67
|
+
'0%': {
|
|
68
|
+
opacity: '0',
|
|
69
|
+
transform: 'translateX(100%)'
|
|
70
|
+
},
|
|
71
|
+
'100%': {
|
|
72
|
+
opacity: '1',
|
|
73
|
+
transform: 'translateX(0)'
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
'toast-out': {
|
|
77
|
+
'0%': {
|
|
78
|
+
opacity: '1',
|
|
79
|
+
transform: 'translateX(0)'
|
|
80
|
+
},
|
|
81
|
+
'100%': {
|
|
82
|
+
opacity: '0',
|
|
83
|
+
transform: 'translateX(100%)'
|
|
84
|
+
}
|
|
85
|
+
},
|
|
66
86
|
'slide-in': {
|
|
67
87
|
'0%': { transform: 'translateX(100%)' },
|
|
68
88
|
'100%': { transform: 'translateX(0)' }
|
|
@@ -74,6 +94,8 @@ const defaultConfig = {
|
|
|
74
94
|
},
|
|
75
95
|
animation: {
|
|
76
96
|
'skeleton-shimmer': 'skeleton-shimmer 2s linear infinite',
|
|
97
|
+
'toast-in': 'toast-in 0.3s ease-out forwards',
|
|
98
|
+
'toast-out': 'toast-out 0.25s ease-in forwards',
|
|
77
99
|
'slide-in': 'slide-in 0.4s ease-out forwards',
|
|
78
100
|
'slide-out': 'slide-out 0.4s ease-out forwards'
|
|
79
101
|
}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
public
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "Project Zero | Next",
|
|
3
|
-
"short_name": "PZ | Next",
|
|
4
|
-
"icons": [
|
|
5
|
-
{
|
|
6
|
-
"src": "/icon-192x192.png",
|
|
7
|
-
"sizes": "192x192",
|
|
8
|
-
"type": "image/png",
|
|
9
|
-
"purpose": "maskable"
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"src": "/icon-256x256.png",
|
|
13
|
-
"sizes": "256x256",
|
|
14
|
-
"type": "image/png",
|
|
15
|
-
"purpose": "any"
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
"src": "/icon-384x384.png",
|
|
19
|
-
"sizes": "384x384",
|
|
20
|
-
"type": "image/png",
|
|
21
|
-
"purpose": "any"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"src": "/icon-512x512.png",
|
|
25
|
-
"sizes": "512x512",
|
|
26
|
-
"type": "image/png",
|
|
27
|
-
"purpose": "any"
|
|
28
|
-
}
|
|
29
|
-
],
|
|
30
|
-
"theme_color": "#FFFFFF",
|
|
31
|
-
"background_color": "#FFFFFF",
|
|
32
|
-
"start_url": "/",
|
|
33
|
-
"display": "standalone",
|
|
34
|
-
"orientation": "portrait"
|
|
35
|
-
}
|