@akinon/projectzero 2.0.10-rc.0 → 2.0.10

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 CHANGED
@@ -1,12 +1,11 @@
1
1
  # @akinon/projectzero
2
2
 
3
- ## 2.0.10-rc.0
3
+ ## 2.0.10
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - 143be2b9d: ZERO-3457: Crop styles are customizable and logic improved for rendering similar products modal
8
- - d99a6a7d5: ZERO-3457_1: Fixed the settings prop and made sure everything is customizable.
9
- - e18836b2: ZERO-4160: Restore scope in Sentry addon configuration in akinon.json
7
+ - 31f07bd7: ZERO-4459: Move migrate-eslint codemod into @akinon/projectzero and integrate into upgrade-to-2
8
+ - 6760888a: ZERO-4461: upgrade-to-2 codemod targets @akinon/\* latest (stable) instead of beta dist-tag
10
9
 
11
10
  ## 2.0.9
12
11
 
@@ -7,7 +7,6 @@ NEXT_PUBLIC_URL=http://localhost:3000
7
7
  SERVICE_BACKEND_URL=https://02fde10fee4440269e695aa10707dfaf.lb.akinoncloud.com
8
8
  SITEMAP_S3_BUCKET_NAME=0fb534
9
9
  NEXT_PUBLIC_VIRTUAL_TRY_ON_API_URL=https://d2a26507c7094f359aba349b96a96881.lb.akinoncloud.com
10
- NEXT_PUBLIC_ENABLE_IMAGE_SEARCH=true
11
10
 
12
11
  # LOG_LEVEL_DEV=debug # For more details, please refer to the Logging documentation.
13
12
 
@@ -1,75 +1,37 @@
1
1
  # projectzeronext
2
2
 
3
- ## 2.0.10-rc.0
4
-
5
- ### Patch Changes
6
-
7
- - 1a8622a9b: ZERO-3392: Implemented AI Search functionality and update env
8
- - b55acb768: ZERO-2577: Fix pagination bug and update usePagination hook and ensure pagination controls rendering correctly
9
- - 143be2b9d: ZERO-3457: Crop styles are customizable and logic improved for rendering similar products modal
10
- - 9f8cd3bc5: ZERO-3449: AI Search Active Filters & Crop Style changes have been implemented
11
- - d99a6a7d5: ZERO-3457_1: Fixed the settings prop and made sure everything is customizable.
12
- - c0228aff: ZERO-4159: Upgrade the package versions
13
- - b05c35435: ZERO-2570: Category filters routes to absolute url
14
- - e18836b2: ZERO-4160: Restore scope in Sentry addon configuration in akinon.json
15
- - c3244a31: ZERO-4013: Update runtime version to node:25-alpine in project configuration
16
- - Updated dependencies [b02e7a7a]
17
- - Updated dependencies [0cf9ea23]
18
- - Updated dependencies [324f97d5]
19
- - Updated dependencies [51ea0688]
20
- - Updated dependencies
21
- - Updated dependencies [eba125b2]
22
- - Updated dependencies [823a4449]
23
- - Updated dependencies [b55acb768]
24
- - Updated dependencies [760258c1]
25
- - Updated dependencies [5da13fff]
26
- - Updated dependencies [143be2b9d]
27
- - Updated dependencies [7889b08f]
28
- - Updated dependencies [fb4aa9b4]
29
- - Updated dependencies [9f8cd3bc5]
30
- - Updated dependencies [bfafa3f4]
31
- - Updated dependencies [00b9f488]
32
- - Updated dependencies [57d7eb30]
33
- - Updated dependencies [d99a6a7d5]
34
- - Updated dependencies [9db81a71]
35
- - Updated dependencies [591e345e]
36
- - Updated dependencies [0df6bc1f]
37
- - Updated dependencies [4de5303c5]
38
- - Updated dependencies [63a72000]
39
- - Updated dependencies [95b139dc1]
40
- - Updated dependencies [1d00f2d0]
41
- - Updated dependencies [4ac7b2a1]
42
- - Updated dependencies [4998a963]
43
- - Updated dependencies [c0228aff]
44
- - Updated dependencies [3909d322]
45
- - Updated dependencies [e18836b2]
46
- - @akinon/pz-flow-payment@2.0.10-rc.0
47
- - @akinon/next@2.0.10-rc.0
48
- - @akinon/pz-checkout-gift-pack@2.0.10-rc.0
49
- - @akinon/pz-one-click-checkout@2.0.10-rc.0
50
- - @akinon/pz-basket-gift-pack@2.0.10-rc.0
51
- - @akinon/pz-masterpass-rest@2.0.10-rc.0
52
- - @akinon/pz-pay-on-delivery@2.0.10-rc.0
53
- - @akinon/pz-credit-payment@2.0.10-rc.0
54
- - @akinon/pz-click-collect@2.0.10-rc.0
55
- - @akinon/pz-multi-basket@2.0.10-rc.0
56
- - @akinon/pz-masterpass@2.0.10-rc.0
57
- - @akinon/pz-gpay@2.0.10-rc.0
58
- - @akinon/pz-b2b@2.0.10-rc.0
59
- - @akinon/pz-bkm@2.0.10-rc.0
60
- - @akinon/pz-otp@2.0.10-rc.0
61
- - @akinon/pz-similar-products@2.0.10-rc.0
62
- - @akinon/pz-virtual-try-on@2.0.10-rc.0
63
- - @akinon/pz-theme@2.0.10-rc.0
64
- - @akinon/pz-akifast@2.0.10-rc.0
65
- - @akinon/pz-apple-pay@2.0.10-rc.0
66
- - @akinon/pz-cybersource-uc@2.0.10-rc.0
67
- - @akinon/pz-google-pay@2.0.10-rc.0
68
- - @akinon/pz-haso@2.0.10-rc.0
69
- - @akinon/pz-hepsipay@2.0.10-rc.0
70
- - @akinon/pz-saved-card@2.0.10-rc.0
71
- - @akinon/pz-tabby-extension@2.0.10-rc.0
72
- - @akinon/pz-tamara-extension@2.0.10-rc.0
3
+ ## 2.0.10
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [31f07bd7]
8
+ - @akinon/next@2.0.10
9
+ - @akinon/pz-theme@2.0.10
10
+ - @akinon/pz-virtual-try-on@2.0.10
11
+ - @akinon/pz-akifast@2.0.10
12
+ - @akinon/pz-apple-pay@2.0.10
13
+ - @akinon/pz-b2b@2.0.10
14
+ - @akinon/pz-basket-gift-pack@2.0.10
15
+ - @akinon/pz-bkm@2.0.10
16
+ - @akinon/pz-checkout-gift-pack@2.0.10
17
+ - @akinon/pz-click-collect@2.0.10
18
+ - @akinon/pz-credit-payment@2.0.10
19
+ - @akinon/pz-cybersource-uc@2.0.10
20
+ - @akinon/pz-flow-payment@2.0.10
21
+ - @akinon/pz-google-pay@2.0.10
22
+ - @akinon/pz-gpay@2.0.10
23
+ - @akinon/pz-haso@2.0.10
24
+ - @akinon/pz-hepsipay@2.0.10
25
+ - @akinon/pz-masterpass@2.0.10
26
+ - @akinon/pz-masterpass-rest@2.0.10
27
+ - @akinon/pz-multi-basket@2.0.10
28
+ - @akinon/pz-one-click-checkout@2.0.10
29
+ - @akinon/pz-otp@2.0.10
30
+ - @akinon/pz-pay-on-delivery@2.0.10
31
+ - @akinon/pz-saved-card@2.0.10
32
+ - @akinon/pz-similar-products@2.0.10
33
+ - @akinon/pz-tabby-extension@2.0.10
34
+ - @akinon/pz-tamara-extension@2.0.10
73
35
 
74
36
  ## 2.0.9
75
37
 
@@ -12,30 +12,6 @@ Run `cp .env.example .env && yarn` command at project root and all dependencies
12
12
 
13
13
  - `SITEMAP_S3_BUCKET_NAME`: S3 bucket name for XML sitemaps (e.g., "0fb534"). This is required for the sitemap route to function correctly.
14
14
 
15
- ### Optional Environment Variables
16
-
17
- - `NEXT_PUBLIC_ENABLE_IMAGE_SEARCH`: Set to `true` to enable image search feature for all users. If not set or `false`, the feature will only be visible to users who have enabled it via localStorage.
18
-
19
- ## Image Search Feature
20
-
21
- The image search functionality allows users to search for similar products by uploading images or cropping existing product images.
22
-
23
- ### Feature Visibility Control
24
-
25
- The image search feature can be controlled in two ways:
26
-
27
- 1. **Environment Variable (Global)**: Set `NEXT_PUBLIC_ENABLE_IMAGE_SEARCH=true` to enable for all users
28
- 2. **localStorage (Individual Users)**: Users can enable it locally using browser developer tools
29
-
30
- ````
31
-
32
- ### Feature Locations
33
-
34
- When enabled, the image search feature appears in:
35
-
36
- - **Header Search**: Camera icon next to the search input
37
- - **Product Detail Page**: "View Similar Styles" button on product images
38
-
39
15
  ### Build
40
16
 
41
17
  To build the app, run the following command:
@@ -43,7 +19,7 @@ To build the app, run the following command:
43
19
  ```bash
44
20
  cd projectzeronext # Project root
45
21
  yarn build
46
- ````
22
+ ```
47
23
 
48
24
  ### Develop
49
25
 
@@ -48,12 +48,9 @@ const withSerwist = withSerwistInit({
48
48
  const sentryConfig = {
49
49
  silent: true,
50
50
  dryRun: !process.env.SENTRY_DSN,
51
- org: 'akinon',
51
+ org: 'akinon'
52
52
  // project: 'enter_your_project_name_here',
53
53
  // authToken: 'enter_your_auth_token_here'
54
- sourcemaps: {
55
- deleteSourcemapsAfterUpload: true
56
- }
57
54
  };
58
55
 
59
56
  const enhancedConfig = withPzConfig(nextConfig);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectzeronext",
3
- "version": "2.0.10-rc.0",
3
+ "version": "2.0.10",
4
4
  "private": true,
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -24,33 +24,33 @@
24
24
  "test:middleware": "jest middleware-matcher.test.ts --bail"
25
25
  },
26
26
  "dependencies": {
27
- "@akinon/next": "2.0.10-rc.0",
28
- "@akinon/pz-akifast": "2.0.10-rc.0",
29
- "@akinon/pz-apple-pay": "2.0.10-rc.0",
30
- "@akinon/pz-b2b": "2.0.10-rc.0",
31
- "@akinon/pz-basket-gift-pack": "2.0.10-rc.0",
32
- "@akinon/pz-bkm": "2.0.10-rc.0",
33
- "@akinon/pz-checkout-gift-pack": "2.0.10-rc.0",
34
- "@akinon/pz-click-collect": "2.0.10-rc.0",
35
- "@akinon/pz-credit-payment": "2.0.10-rc.0",
36
- "@akinon/pz-cybersource-uc": "2.0.10-rc.0",
37
- "@akinon/pz-flow-payment": "2.0.10-rc.0",
38
- "@akinon/pz-google-pay": "2.0.10-rc.0",
39
- "@akinon/pz-gpay": "2.0.10-rc.0",
40
- "@akinon/pz-haso": "2.0.10-rc.0",
41
- "@akinon/pz-hepsipay": "2.0.10-rc.0",
42
- "@akinon/pz-masterpass": "2.0.10-rc.0",
43
- "@akinon/pz-masterpass-rest": "2.0.10-rc.0",
44
- "@akinon/pz-multi-basket": "2.0.10-rc.0",
45
- "@akinon/pz-one-click-checkout": "2.0.10-rc.0",
46
- "@akinon/pz-otp": "2.0.10-rc.0",
47
- "@akinon/pz-pay-on-delivery": "2.0.10-rc.0",
48
- "@akinon/pz-saved-card": "2.0.10-rc.0",
49
- "@akinon/pz-similar-products": "2.0.10-rc.0",
50
- "@akinon/pz-tabby-extension": "2.0.10-rc.0",
51
- "@akinon/pz-tamara-extension": "2.0.10-rc.0",
52
- "@akinon/pz-theme": "2.0.10-rc.0",
53
- "@akinon/pz-virtual-try-on": "2.0.10-rc.0",
27
+ "@akinon/next": "2.0.10",
28
+ "@akinon/pz-akifast": "2.0.10",
29
+ "@akinon/pz-apple-pay": "2.0.10",
30
+ "@akinon/pz-b2b": "2.0.10",
31
+ "@akinon/pz-basket-gift-pack": "2.0.10",
32
+ "@akinon/pz-bkm": "2.0.10",
33
+ "@akinon/pz-checkout-gift-pack": "2.0.10",
34
+ "@akinon/pz-click-collect": "2.0.10",
35
+ "@akinon/pz-credit-payment": "2.0.10",
36
+ "@akinon/pz-cybersource-uc": "2.0.10",
37
+ "@akinon/pz-flow-payment": "2.0.10",
38
+ "@akinon/pz-google-pay": "2.0.10",
39
+ "@akinon/pz-gpay": "2.0.10",
40
+ "@akinon/pz-haso": "2.0.10",
41
+ "@akinon/pz-hepsipay": "2.0.10",
42
+ "@akinon/pz-masterpass": "2.0.10",
43
+ "@akinon/pz-masterpass-rest": "2.0.10",
44
+ "@akinon/pz-multi-basket": "2.0.10",
45
+ "@akinon/pz-one-click-checkout": "2.0.10",
46
+ "@akinon/pz-otp": "2.0.10",
47
+ "@akinon/pz-pay-on-delivery": "2.0.10",
48
+ "@akinon/pz-saved-card": "2.0.10",
49
+ "@akinon/pz-similar-products": "2.0.10",
50
+ "@akinon/pz-tabby-extension": "2.0.10",
51
+ "@akinon/pz-tamara-extension": "2.0.10",
52
+ "@akinon/pz-theme": "2.0.10",
53
+ "@akinon/pz-virtual-try-on": "2.0.10",
54
54
  "@hookform/resolvers": "2.9.0",
55
55
  "@next/third-parties": "16.2.6",
56
56
  "@react-google-maps/api": "2.17.1",
@@ -59,7 +59,7 @@
59
59
  "next": "16.2.6",
60
60
  "@serwist/next": "9.5.11",
61
61
  "next-auth": "5.0.0-beta.30",
62
- "pino": "8.21.0",
62
+ "pino": "8.11.0",
63
63
  "serwist": "9.5.11",
64
64
  "postcss": "8.4.49",
65
65
  "react": "19.2.5",
@@ -67,14 +67,14 @@
67
67
  "react-google-recaptcha": "2.1.0",
68
68
  "react-hook-form": "7.71.2",
69
69
  "react-intersection-observer": "9.4.0",
70
- "react-multi-carousel": "2.8.5",
71
- "react-string-replace": "1.1.1",
72
- "start-server-and-test": "2.0.8",
73
- "tailwind-merge": "1.14.0",
70
+ "react-multi-carousel": "2.8.4",
71
+ "react-string-replace": "1.1.0",
72
+ "start-server-and-test": "2.0.3",
73
+ "tailwind-merge": "1.8.0",
74
74
  "yup": "0.32.11"
75
75
  },
76
76
  "devDependencies": {
77
- "@akinon/eslint-plugin-projectzero": "2.0.10-rc.0",
77
+ "@akinon/eslint-plugin-projectzero": "2.0.10",
78
78
  "@semantic-release/changelog": "6.0.2",
79
79
  "@semantic-release/exec": "6.0.3",
80
80
  "@semantic-release/git": "10.0.1",
@@ -89,25 +89,25 @@
89
89
  "@types/react": "19.2.14",
90
90
  "@types/react-dom": "19.2.3",
91
91
  "client-only": "0.0.1",
92
- "clsx": "1.2.1",
92
+ "clsx": "1.1.1",
93
93
  "currency-symbol-map": "5.1.0",
94
94
  "eslint": "9.39.4",
95
95
  "eslint-config-next": "16.2.4",
96
96
  "eslint-config-prettier": "10.1.1",
97
97
  "husky": "8.0.0",
98
98
  "jest": "29.7.0",
99
- "jest-css-modules-transform": "4.4.2",
99
+ "jest-css-modules-transform": "4.3.0",
100
100
  "jest-environment-jsdom": "29.7.0",
101
- "lint-staged": "13.3.0",
102
- "prettier": "2.8.8",
101
+ "lint-staged": "13.1.0",
102
+ "prettier": "2.6.2",
103
103
  "react-number-format": "5.3.4",
104
- "sass": "1.83.0",
104
+ "sass": "1.49.9",
105
105
  "semantic-release": "19.0.5",
106
106
  "server-only": "0.0.1",
107
- "stylelint": "14.16.1",
107
+ "stylelint": "14.6.0",
108
108
  "stylelint-config-sass-guidelines": "9.0.1",
109
109
  "stylelint-config-standard": "25.0.0",
110
- "stylelint-scss": "4.7.0",
110
+ "stylelint-scss": "4.2.0",
111
111
  "stylelint-selector-bem-pattern": "2.1.1",
112
112
  "tailwindcss": "4.2.0",
113
113
  "ts-jest": "29.2.6",
@@ -1,3 +1 @@
1
- export * from './use-contract';
2
- export * from './use-fav-button';
3
1
  export * from './use-add-product-to-basket';
@@ -19,7 +19,6 @@ module.exports = [
19
19
  'pz-hepsipay',
20
20
  'pz-flow-payment',
21
21
  'pz-virtual-try-on',
22
- 'pz-similar-products',
23
22
  'pz-cybersource-uc',
24
23
  'pz-hepsipay',
25
24
  'pz-masterpass-rest',
@@ -14,8 +14,7 @@ export const config = {
14
14
  '/((?!api|_next|[\\w-\\/*]+\\.\\w+).*)',
15
15
  '/(.*sitemap\\.xml)',
16
16
  '/(.+\\.)(html|htm|aspx|asp|php)',
17
- '/(.*orders\\/checkout-with-token.*)',
18
- '/(.*orders\\/post-checkout.*)'
17
+ '/(.*orders\\/checkout-with-token.*)'
19
18
  ]
20
19
  };
21
20
 
@@ -4,7 +4,8 @@ import { useEffect, useRef, useState } from 'react';
4
4
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
5
5
  import { closeSearch } from '@akinon/next/redux/reducers/header';
6
6
  import clsx from 'clsx';
7
- import { Icon, Input } from '@theme/components';
7
+
8
+ import { Icon } from '@theme/components';
8
9
  import Results from './results';
9
10
  import { ROUTES } from '@theme/routes';
10
11
  import { useLocalization, useRouter } from '@akinon/next/hooks';
@@ -41,14 +42,6 @@ export default function Search() {
41
42
  };
42
43
  }, [isSearchOpen, dispatch]);
43
44
 
44
- const handleSearchTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
45
- setSearchText(e.target.value);
46
- };
47
-
48
- const handleCloseSearch = () => {
49
- dispatch(closeSearch());
50
- };
51
-
52
45
  return (
53
46
  <>
54
47
  <div
@@ -74,9 +67,9 @@ export default function Search() {
74
67
  {t('common.search.results_for')}
75
68
  </span>
76
69
  <div className="flex items-center">
77
- <Input
70
+ <input
78
71
  value={searchText}
79
- onChange={handleSearchTextChange}
72
+ onChange={(e) => setSearchText(e.target.value)}
80
73
  onKeyDown={(e) => {
81
74
  if (e.key === 'Enter' && searchText.trim() !== '') {
82
75
  router.push(`${ROUTES.LIST}/?search_text=${searchText}`);
@@ -98,12 +91,11 @@ export default function Search() {
98
91
  <Icon
99
92
  name="close"
100
93
  size={14}
101
- onClick={handleCloseSearch}
94
+ onClick={() => dispatch(closeSearch())}
102
95
  className="cursor-pointer"
103
96
  />
104
97
  </div>
105
98
  </div>
106
-
107
99
  <Results searchText={searchText} />
108
100
  </div>
109
101
  </div>
@@ -36,100 +36,53 @@ export default function ProductInfoSlider({ product }: ProductSliderItem) {
36
36
  carouselRef.current?.next();
37
37
  };
38
38
 
39
- const handleThumbnailClick = (index: number) => {
39
+ const handleThumbnailClick = (index) => {
40
40
  setActiveIndex(index);
41
41
  carouselRef.current?.goToSlide(index);
42
42
  };
43
43
 
44
44
  return (
45
- <>
46
- <div className="lg:grid lg:grid-cols-6">
47
- <div className="lg:col-span-1">
48
- <div className="flex flex-col items-center justify-center md:mr-[6px]">
49
- <button
50
- onClick={goToPrev}
51
- className={twMerge(
52
- 'hidden justify-center p-2 mb-3 border border-gray-100 rounded-full cursor-pointer lg:block',
53
- [activeIndex === 0 && 'cursor-not-allowed opacity-45']
54
- )}
55
- disabled={activeIndex === 0}
56
- >
57
- <Icon name="chevron-up" size={15} className="fill-[#000000]" />
58
- </button>
59
- <div className="hidden flex-col items-center overflow-scroll w-[80px] max-h-[620px] lg:block">
60
- {product?.productimage_set?.map((item, index) => (
61
- <Image
62
- key={index}
63
- src={item.image}
64
- alt={`Thumbnail ${index}`}
65
- width={80}
66
- height={128}
67
- aspectRatio={80 / 128}
68
- className={twMerge('cursor-pointer', [
69
- activeIndex === index && 'border-2 border-primary'
70
- ])}
71
- onClick={() => handleThumbnailClick(index)}
72
- />
73
- ))}
74
- </div>
75
- <button
76
- onClick={goToNext}
77
- className={twMerge(
78
- 'hidden justify-center p-2 mt-3 border border-gray-100 rounded-full cursor-pointer lg:block',
79
- [
80
- activeIndex === product.productimage_set.length - 1 &&
81
- 'cursor-not-allowed opacity-45'
82
- ]
83
- )}
84
- disabled={activeIndex === product.productimage_set.length - 1}
85
- >
86
- <Icon name="chevron-down" size={15} className="fill-[#000000]" />
87
- </button>
88
- </div>
89
- </div>
90
-
91
- <div className="relative lg:col-span-5">
92
- <FavButton className="absolute right-8 top-6 z-[20] sm:hidden" />
93
-
94
- <PluginModule
95
- component={Component.ProductImageSearchFeature}
96
- props={{
97
- product,
98
- activeIndex,
99
- showResetButton: true
100
- }}
101
- />
102
-
103
- <CarouselCore
104
- responsive={{
105
- all: {
106
- breakpoint: { max: 5000, min: 0 },
107
- items: 1
108
- }
109
- }}
110
- arrows={false}
111
- swipeable={true}
112
- ref={carouselRef}
113
- afterChange={(previousSlide, { currentSlide }) => {
114
- setActiveIndex(currentSlide);
115
- }}
116
- containerAspectRatio={{ mobile: 520 / 798, desktop: 484 / 726 }}
45
+ <div className="lg:grid lg:grid-cols-6">
46
+ <div className="lg:col-span-1">
47
+ <div className="flex flex-col items-center justify-center md:mr-[6px]">
48
+ <button
49
+ onClick={goToPrev}
50
+ className={twMerge(
51
+ 'hidden justify-center p-2 mb-3 border border-gray-100 rounded-full cursor-pointer lg:block',
52
+ [activeIndex === 0 && 'cursor-not-allowed opacity-45']
53
+ )}
54
+ disabled={activeIndex === 0}
117
55
  >
118
- {product?.productimage_set?.map((item, i) => (
56
+ <Icon name="chevron-up" size={15} className="fill-[#000000]" />
57
+ </button>
58
+ <div className="hidden flex-col items-center overflow-scroll w-[80px] max-h-[620px] lg:block">
59
+ {product?.productimage_set?.map((item, index) => (
119
60
  <Image
120
- key={i}
61
+ key={index}
121
62
  src={item.image}
122
- alt={product?.name || 'Product image'}
123
- draggable={false}
124
- aspectRatio={484 / 726}
125
- sizes="(min-width: 425px) 512px,
126
- (min-width: 601px) 576px,
127
- (min-width: 768px) 336px,
128
- (min-width: 1024px) 484px, 368px"
129
- fill
63
+ alt={`Thumbnail ${index}`}
64
+ width={80}
65
+ height={128}
66
+ className={twMerge('cursor-pointer', [
67
+ activeIndex === index && 'border-2 border-primary'
68
+ ])}
69
+ onClick={() => handleThumbnailClick(index)}
130
70
  />
131
71
  ))}
132
- </CarouselCore>
72
+ </div>
73
+ <button
74
+ onClick={goToNext}
75
+ className={twMerge(
76
+ 'hidden justify-center p-2 mt-3 border border-gray-100 rounded-full cursor-pointer lg:block',
77
+ [
78
+ activeIndex === product.productimage_set.length - 1 &&
79
+ 'cursor-not-allowed opacity-45'
80
+ ]
81
+ )}
82
+ disabled={activeIndex === product.productimage_set.length - 1}
83
+ >
84
+ <Icon name="chevron-down" size={15} className="fill-[#000000]" />
85
+ </button>
133
86
  </div>
134
87
  </div>
135
88
 
@@ -187,6 +140,6 @@ export default function ProductInfoSlider({ product }: ProductSliderItem) {
187
140
  ))}
188
141
  </CarouselCore>
189
142
  </div>
190
- </>
143
+ </div>
191
144
  );
192
145
  }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * ESLint flat config migration codemod for Project Zero brands.
3
+ *
4
+ * Converts a brand project from legacy `.eslintrc.js` to ESLint v9 flat config
5
+ * (`eslint.config.mjs`), consuming the shared base from
6
+ * `@akinon/next/eslint.config.base.mjs`.
7
+ *
8
+ * Idempotent: re-running on an already migrated project is a no-op for
9
+ * already-applied steps.
10
+ *
11
+ * Steps:
12
+ * 1. Read .eslintrc.js (if present) to detect brand-specific settings
13
+ * (settings.next.rootDir, custom rules).
14
+ * 2. Read .eslintignore (if present) to collect ignore patterns.
15
+ * 3. Write eslint.config.mjs (skipped if already exists).
16
+ * 4. Delete .eslintrc.js and .eslintignore.
17
+ * 5. Rewrite .lintstagedrc.js to use `eslint --fix` instead of `next lint`.
18
+ * 6. Patch package.json:
19
+ * - scripts.lint: "next lint" -> "eslint ."
20
+ * - eslint: ^8.x -> ^9.39.4
21
+ * - eslint-config-prettier: ^8.x -> ^10.1.1
22
+ * - remove @typescript-eslint/eslint-plugin and @typescript-eslint/parser
23
+ * (now provided by eslint-config-next/typescript).
24
+ *
25
+ * Usage:
26
+ * cd <brand-root>
27
+ * npx @akinon/projectzero codemod --codemod=migrate-eslint
28
+ */
29
+
30
+ 'use strict';
31
+
32
+ const fs = require('fs');
33
+ const path = require('path');
34
+
35
+ const ESLINT_TARGET = '9.39.4';
36
+ const ESLINT_CONFIG_PRETTIER_TARGET = '10.1.1';
37
+ const TYPESCRIPT_ESLINT_DROP = [
38
+ '@typescript-eslint/eslint-plugin',
39
+ '@typescript-eslint/parser'
40
+ ];
41
+
42
+ function log(msg) {
43
+ const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
44
+ console.log(`[${ts}] [migrate-eslint] ${msg}`);
45
+ }
46
+
47
+ function buildFlatConfig({ rootDir, ignores, customRules }) {
48
+ const lines = [
49
+ "import { defineConfig, globalIgnores } from 'eslint/config';",
50
+ "import baseConfig from '@akinon/next/eslint.config.base.mjs';",
51
+ '',
52
+ 'export default defineConfig([',
53
+ ' ...baseConfig'
54
+ ];
55
+
56
+ if (rootDir) {
57
+ lines[lines.length - 1] += ',';
58
+ lines.push(
59
+ ' {',
60
+ ' settings: {',
61
+ ' next: {',
62
+ ` rootDir: ${JSON.stringify(rootDir)}`,
63
+ ' }',
64
+ ' }',
65
+ ' }'
66
+ );
67
+ }
68
+
69
+ if (customRules) {
70
+ lines[lines.length - 1] += ',';
71
+ const indented = JSON.stringify(customRules, null, 2)
72
+ .split('\n')
73
+ .map((l, i) => (i === 0 ? ' rules: ' + l : ' ' + l))
74
+ .join('\n');
75
+ lines.push(' {', indented, ' }');
76
+ }
77
+
78
+ if (ignores && ignores.length) {
79
+ lines[lines.length - 1] += ',';
80
+ lines.push(` globalIgnores(${JSON.stringify(ignores)})`);
81
+ }
82
+
83
+ lines.push(']);', '');
84
+
85
+ return lines.join('\n');
86
+ }
87
+
88
+ const LINT_STAGED_TEMPLATE = `const path = require('path');
89
+
90
+ const buildEslintCommand = (filenames) =>
91
+ \`eslint --fix \${filenames
92
+ .map((f) => \`"\${path.relative(process.cwd(), f)}"\`)
93
+ .join(' ')}\`;
94
+
95
+ module.exports = {
96
+ '**/*.(ts|tsx)': () => 'yarn tsc --noEmit',
97
+ '*.{js,jsx,ts,tsx}': [buildEslintCommand, 'prettier --write'],
98
+ '*.{css,scss}': [
99
+ 'stylelint --fix --allow-empty-input',
100
+ 'prettier --write',
101
+ 'stylelint --allow-empty-input'
102
+ ]
103
+ };
104
+ `;
105
+
106
+ function loadLegacyConfig(eslintrcPath) {
107
+ try {
108
+ delete require.cache[require.resolve(eslintrcPath)];
109
+ return require(eslintrcPath);
110
+ } catch (err) {
111
+ return { __parseError: err.message };
112
+ }
113
+ }
114
+
115
+ function readIgnoreFile(eslintignorePath) {
116
+ if (!fs.existsSync(eslintignorePath)) return [];
117
+ const raw = fs.readFileSync(eslintignorePath, 'utf8');
118
+ return raw
119
+ .split(/\r?\n/)
120
+ .map((line) => line.trim())
121
+ .filter((line) => line && !line.startsWith('#'))
122
+ .map((pattern) => (pattern.endsWith('/**') ? pattern : pattern + '/**'));
123
+ }
124
+
125
+ function patchPackageJson(pkgPath, { dryRun }) {
126
+ const raw = fs.readFileSync(pkgPath, 'utf8');
127
+ const pkg = JSON.parse(raw);
128
+ const changes = [];
129
+
130
+ if (pkg.scripts && typeof pkg.scripts.lint === 'string') {
131
+ if (/^next\s+lint\b/.test(pkg.scripts.lint.trim())) {
132
+ pkg.scripts.lint = 'eslint .';
133
+ changes.push('scripts.lint -> "eslint ."');
134
+ }
135
+ }
136
+
137
+ const bumpDev = (name, target) => {
138
+ if (!pkg.devDependencies || !pkg.devDependencies[name]) return;
139
+ const current = pkg.devDependencies[name].replace(/^\^|^~/, '');
140
+ if (current === target) return;
141
+ pkg.devDependencies[name] = target;
142
+ changes.push(`devDependencies.${name} ${current} -> ${target}`);
143
+ };
144
+
145
+ bumpDev('eslint', ESLINT_TARGET);
146
+ bumpDev('eslint-config-prettier', ESLINT_CONFIG_PRETTIER_TARGET);
147
+
148
+ for (const dep of TYPESCRIPT_ESLINT_DROP) {
149
+ if (pkg.devDependencies && pkg.devDependencies[dep]) {
150
+ delete pkg.devDependencies[dep];
151
+ changes.push(`removed devDependencies.${dep}`);
152
+ }
153
+ }
154
+
155
+ if (changes.length === 0) return { changed: false, changes };
156
+
157
+ if (!dryRun) {
158
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
159
+ }
160
+ return { changed: true, changes };
161
+ }
162
+
163
+ const transform = () => {
164
+ const root = path.resolve(process.cwd());
165
+ const dryRun = process.argv.includes('--dry-run');
166
+
167
+ const eslintrcPath = path.join(root, '.eslintrc.js');
168
+ const eslintignorePath = path.join(root, '.eslintignore');
169
+ const flatConfigPath = path.join(root, 'eslint.config.mjs');
170
+ const lintStagedPath = path.join(root, '.lintstagedrc.js');
171
+ const packageJsonPath = path.join(root, 'package.json');
172
+
173
+ if (!fs.existsSync(packageJsonPath)) {
174
+ log(`No package.json found in ${root}`);
175
+ process.exitCode = 1;
176
+ return;
177
+ }
178
+
179
+ log(`starting in ${root}${dryRun ? ' (DRY RUN)' : ''}`);
180
+ const summary = [];
181
+
182
+ const legacyConfig = fs.existsSync(eslintrcPath)
183
+ ? loadLegacyConfig(eslintrcPath)
184
+ : null;
185
+
186
+ if (legacyConfig && legacyConfig.__parseError) {
187
+ log(
188
+ `WARN: could not parse .eslintrc.js (${legacyConfig.__parseError}); continuing with default brand template`
189
+ );
190
+ }
191
+
192
+ const rootDir = legacyConfig?.settings?.next?.rootDir;
193
+ const customRules =
194
+ legacyConfig && legacyConfig.rules && Object.keys(legacyConfig.rules).length
195
+ ? legacyConfig.rules
196
+ : null;
197
+
198
+ let ignores = readIgnoreFile(eslintignorePath);
199
+ if (ignores.length === 0) ignores = ['public/**'];
200
+
201
+ if (!fs.existsSync(flatConfigPath)) {
202
+ const body = buildFlatConfig({ rootDir, ignores, customRules });
203
+ if (dryRun) {
204
+ log(`[DRY] CREATE ${flatConfigPath} (${body.length} bytes)`);
205
+ } else {
206
+ fs.writeFileSync(flatConfigPath, body, 'utf8');
207
+ }
208
+ summary.push('created eslint.config.mjs');
209
+ } else {
210
+ summary.push('eslint.config.mjs already exists (skipped)');
211
+ }
212
+
213
+ if (fs.existsSync(eslintrcPath)) {
214
+ if (dryRun) {
215
+ log(`[DRY] DELETE ${eslintrcPath}`);
216
+ } else {
217
+ fs.unlinkSync(eslintrcPath);
218
+ }
219
+ summary.push('removed .eslintrc.js');
220
+ }
221
+
222
+ if (fs.existsSync(eslintignorePath)) {
223
+ if (dryRun) {
224
+ log(`[DRY] DELETE ${eslintignorePath}`);
225
+ } else {
226
+ fs.unlinkSync(eslintignorePath);
227
+ }
228
+ summary.push('removed .eslintignore');
229
+ }
230
+
231
+ if (fs.existsSync(lintStagedPath)) {
232
+ const before = fs.readFileSync(lintStagedPath, 'utf8');
233
+ if (before !== LINT_STAGED_TEMPLATE) {
234
+ if (dryRun) {
235
+ log(`[DRY] REWRITE ${lintStagedPath}`);
236
+ } else {
237
+ fs.writeFileSync(lintStagedPath, LINT_STAGED_TEMPLATE, 'utf8');
238
+ }
239
+ summary.push('rewrote .lintstagedrc.js');
240
+ } else {
241
+ summary.push('.lintstagedrc.js already up to date (skipped)');
242
+ }
243
+ }
244
+
245
+ const pkgResult = patchPackageJson(packageJsonPath, { dryRun });
246
+ if (pkgResult.changed) {
247
+ summary.push(`patched package.json (${pkgResult.changes.join(', ')})`);
248
+ } else {
249
+ summary.push('package.json already up to date (skipped)');
250
+ }
251
+
252
+ console.log('\nESLint flat config migration summary:');
253
+ for (const line of summary) console.log(` - ${line}`);
254
+
255
+ if (customRules) {
256
+ console.log(
257
+ `\nNote: ${Object.keys(customRules).length} custom rule(s) from the old ` +
258
+ '.eslintrc.js were carried over into eslint.config.mjs.\nReview if ' +
259
+ 'they are still needed (some may already be covered by the base config).'
260
+ );
261
+ }
262
+
263
+ log('done');
264
+ };
265
+
266
+ module.exports = { transform };
267
+
268
+ if (require.main === module) {
269
+ transform();
270
+ }
@@ -1,17 +1,18 @@
1
1
  /**
2
2
  * upgrade-to-2: orchestrator codemod that chains every step required to move
3
- * a Project Zero brand project from pz-next 1.x to 2.0 beta.
3
+ * a Project Zero brand project from pz-next 1.x to 2.0 stable.
4
4
  *
5
5
  * Steps (in order):
6
- * 1. Preflight checks (git state, package.json, Node.js project)
7
- * 2. Create a backup git branch (pre-upgrade-2-YYYYMMDD)
8
- * 3. Bump package.json dependencies to the 2.0 target matrix
9
- * 4. Run @next/codemod upgrade (Next 16 + React 19)
10
- * 5. Run migrate-auth-v5 codemod (next-auth v4 -> v5)
11
- * 6. Run sentry-9 codemod (only when @sentry/nextjs is present)
12
- * 7. yarn clean && yarn install
13
- * 8. yarn build (validation)
14
- * 9. Summary report
6
+ * 1. Preflight checks (git state, package.json, Node.js project)
7
+ * 2. Create a backup git branch (pre-upgrade-2-YYYYMMDD)
8
+ * 3. Bump package.json dependencies to the 2.0 target matrix
9
+ * 4. yarn clean && yarn install
10
+ * 5. Run @next/codemod upgrade (Next 16 + React 19)
11
+ * 6. Run migrate-auth-v5 codemod (next-auth v4 -> v5)
12
+ * 7. Run sentry-9 codemod (only when @sentry/nextjs is present)
13
+ * 8. Run migrate-eslint codemod (ESLint v9 flat config)
14
+ * 9. yarn build (validation)
15
+ * 10. Summary report
15
16
  *
16
17
  * Out of scope in this first version:
17
18
  * - Tailwind v3 -> v4 migration (run manually with @tailwindcss/upgrade)
@@ -30,6 +31,7 @@
30
31
  * --skip-next-codemod Do not run @next/codemod upgrade
31
32
  * --skip-auth Do not run migrate-auth-v5
32
33
  * --skip-sentry Do not run sentry-9 (auto-skipped if not needed)
34
+ * --skip-eslint Do not run migrate-eslint
33
35
  * --skip-install Do not run yarn install
34
36
  * --skip-build Do not run yarn build
35
37
  * --interactive Pause for confirmation after each step
@@ -43,6 +45,7 @@ const readline = require('readline');
43
45
 
44
46
  const AUTH_CODEMOD = path.resolve(__dirname, '..', 'migrate-auth-v5');
45
47
  const SENTRY_CODEMOD = path.resolve(__dirname, '..', 'sentry-9');
48
+ const ESLINT_CODEMOD = path.resolve(__dirname, '..', 'migrate-eslint');
46
49
 
47
50
  const TARGET_MATRIX = {
48
51
  dependencies: {
@@ -50,22 +53,31 @@ const TARGET_MATRIX = {
50
53
  react: '19.2.5',
51
54
  'react-dom': '19.2.5',
52
55
  'next-auth': '5.0.0-beta.25',
53
- '@akinon/next': 'beta'
56
+ '@akinon/next': 'latest'
54
57
  },
55
58
  devDependencies: {
56
59
  '@types/react': '19.2.14',
57
60
  '@types/react-dom': '19.2.3',
58
61
  'eslint-config-next': '16.2.4',
62
+ eslint: '9.39.4',
63
+ 'eslint-config-prettier': '10.1.1',
59
64
  tailwindcss: '3.4.17',
60
- '@akinon/projectzero': 'beta',
61
- '@akinon/eslint-plugin-projectzero': 'beta',
62
- '@akinon/tsconfig': 'beta'
65
+ '@akinon/projectzero': 'latest',
66
+ '@akinon/eslint-plugin-projectzero': 'latest',
67
+ '@akinon/tsconfig': 'latest'
63
68
  },
64
69
  conditional: {
65
70
  '@sentry/nextjs': '^9.0.0'
66
71
  }
67
72
  };
68
73
 
74
+ // Packages removed when migrating to ESLint v9 / flat config; their rules are
75
+ // now provided by `eslint-config-next/typescript`.
76
+ const DEP_DROP = [
77
+ '@typescript-eslint/eslint-plugin',
78
+ '@typescript-eslint/parser'
79
+ ];
80
+
69
81
  const STEP_COLUMN_WIDTH = 32;
70
82
 
71
83
  function log(msg) {
@@ -114,6 +126,7 @@ function parseArgs(argv) {
114
126
  skipNextCodemod: argv.includes('--skip-next-codemod'),
115
127
  skipAuth: argv.includes('--skip-auth'),
116
128
  skipSentry: argv.includes('--skip-sentry'),
129
+ skipEslint: argv.includes('--skip-eslint'),
117
130
  skipInstall: argv.includes('--skip-install'),
118
131
  skipBuild: argv.includes('--skip-build'),
119
132
  interactive: argv.includes('--interactive'),
@@ -134,7 +147,7 @@ function writePackageJson(cwd, pkg) {
134
147
  }
135
148
 
136
149
  function stepPreflight(cwd, flags) {
137
- logStep('[1/9] Preflight', 'starting');
150
+ logStep('[1/10] Preflight', 'starting');
138
151
 
139
152
  const pkg = readPackageJson(cwd);
140
153
  if (!pkg) {
@@ -166,21 +179,21 @@ function stepPreflight(cwd, flags) {
166
179
  const currentAkinon = pkg.dependencies?.['@akinon/next'] || 'unknown';
167
180
 
168
181
  log(` Current: next@${currentNext} react@${currentReact} next-auth@${currentAuth} @akinon/next@${currentAkinon}`);
169
- log(` Target: next@${TARGET_MATRIX.dependencies.next} react@${TARGET_MATRIX.dependencies.react} next-auth@${TARGET_MATRIX.dependencies['next-auth']} @akinon/next@beta`);
182
+ log(` Target: next@${TARGET_MATRIX.dependencies.next} react@${TARGET_MATRIX.dependencies.react} next-auth@${TARGET_MATRIX.dependencies['next-auth']} @akinon/next@${TARGET_MATRIX.dependencies['@akinon/next']}`);
170
183
 
171
- logStep('[1/9] Preflight', 'ok');
184
+ logStep('[1/10] Preflight', 'ok');
172
185
  }
173
186
 
174
187
  function stepBackup(cwd, flags) {
175
188
  if (flags.skipBackup) {
176
- logStep('[2/9] Backup branch', 'skipped');
189
+ logStep('[2/10] Backup branch', 'skipped');
177
190
  return null;
178
191
  }
179
192
  const today = new Date().toISOString().slice(0, 10).replace(/-/g, '');
180
193
  const branchName = `pre-upgrade-2-${today}`;
181
194
 
182
195
  if (flags.dryRun) {
183
- logStep('[2/9] Backup branch', `[DRY] git branch ${branchName}`);
196
+ logStep('[2/10] Backup branch', `[DRY] git branch ${branchName}`);
184
197
  return branchName;
185
198
  }
186
199
 
@@ -190,7 +203,7 @@ function stepBackup(cwd, flags) {
190
203
  { cwd, encoding: 'utf-8' }
191
204
  );
192
205
  if (existing.status === 0) {
193
- logStep('[2/9] Backup branch', `already exists: ${branchName}`);
206
+ logStep('[2/10] Backup branch', `already exists: ${branchName}`);
194
207
  return branchName;
195
208
  }
196
209
 
@@ -201,7 +214,7 @@ function stepBackup(cwd, flags) {
201
214
  if (result.status !== 0) {
202
215
  throw new Error(`Failed to create backup branch ${branchName}`);
203
216
  }
204
- logStep('[2/9] Backup branch', `created ${branchName}`);
217
+ logStep('[2/10] Backup branch', `created ${branchName}`);
205
218
  return branchName;
206
219
  }
207
220
 
@@ -217,6 +230,21 @@ function bumpDepsSection(section, matrix) {
217
230
  return { section, changes };
218
231
  }
219
232
 
233
+ function dropDeps(pkg, names) {
234
+ const changes = [];
235
+ for (const name of names) {
236
+ if (pkg.dependencies && pkg.dependencies[name]) {
237
+ delete pkg.dependencies[name];
238
+ changes.push(`removed dependencies.${name}`);
239
+ }
240
+ if (pkg.devDependencies && pkg.devDependencies[name]) {
241
+ delete pkg.devDependencies[name];
242
+ changes.push(`removed devDependencies.${name}`);
243
+ }
244
+ }
245
+ return changes;
246
+ }
247
+
220
248
  function pinNextScriptsToWebpack(pkg) {
221
249
  // Next 16 defaults build/dev to Turbopack, which surfaces bundle-time
222
250
  // errors for Node APIs used on the edge runtime (e.g. CompressionStream
@@ -244,7 +272,7 @@ function pinNextScriptsToWebpack(pkg) {
244
272
 
245
273
  function stepBump(cwd, flags) {
246
274
  if (flags.skipBump) {
247
- logStep('[3/9] Bump deps', 'skipped');
275
+ logStep('[3/10] Bump deps', 'skipped');
248
276
  return { hasSentry: false, changes: [] };
249
277
  }
250
278
 
@@ -271,20 +299,23 @@ function stepBump(cwd, flags) {
271
299
  pkg.dependencies = sentryDeps.section;
272
300
  }
273
301
 
302
+ const dropChanges = dropDeps(pkg, DEP_DROP);
303
+ allChanges.push(...dropChanges);
304
+
274
305
  const scriptChanges = pinNextScriptsToWebpack(pkg);
275
306
  allChanges.push(...scriptChanges);
276
307
 
277
308
  if (allChanges.length === 0) {
278
- logStep('[3/9] Bump deps', 'already at target, no changes');
309
+ logStep('[3/10] Bump deps', 'already at target, no changes');
279
310
  return { hasSentry, changes: [] };
280
311
  }
281
312
 
282
313
  if (flags.dryRun) {
283
- logStep('[3/9] Bump deps', `[DRY] ${allChanges.length} changes`);
314
+ logStep('[3/10] Bump deps', `[DRY] ${allChanges.length} changes`);
284
315
  allChanges.forEach((c) => log(` - ${c}`));
285
316
  } else {
286
317
  writePackageJson(cwd, pkg);
287
- logStep('[3/9] Bump deps', `${allChanges.length} changes applied`);
318
+ logStep('[3/10] Bump deps', `${allChanges.length} changes applied`);
288
319
  if (flags.verbose) allChanges.forEach((c) => log(` - ${c}`));
289
320
  }
290
321
 
@@ -293,28 +324,28 @@ function stepBump(cwd, flags) {
293
324
 
294
325
  function stepNextCodemod(cwd, flags) {
295
326
  if (flags.skipNextCodemod) {
296
- logStep('[5/9] Next codemod', 'skipped');
327
+ logStep('[5/10] Next codemod', 'skipped');
297
328
  return { ok: true };
298
329
  }
299
330
  const cmd = flags.dryRun
300
331
  ? 'npx --yes @next/codemod@latest upgrade latest --verbose'
301
332
  : 'npx --yes @next/codemod@latest upgrade latest';
302
- logStep('[5/9] Next codemod', `running ${cmd}`);
333
+ logStep('[5/10] Next codemod', `running ${cmd}`);
303
334
  const result = runCommand(cmd, { cwd, dryRun: false, verbose: flags.verbose });
304
335
  if (result.status !== 0) {
305
- logStep('[5/9] Next codemod', `FAILED (exit ${result.status})`);
336
+ logStep('[5/10] Next codemod', `FAILED (exit ${result.status})`);
306
337
  return { ok: false };
307
338
  }
308
- logStep('[5/9] Next codemod', 'ok');
339
+ logStep('[5/10] Next codemod', 'ok');
309
340
  return { ok: true };
310
341
  }
311
342
 
312
343
  function stepAuthCodemod(cwd, flags) {
313
344
  if (flags.skipAuth) {
314
- logStep('[6/9] Auth codemod', 'skipped');
345
+ logStep('[6/10] Auth codemod', 'skipped');
315
346
  return { ok: true };
316
347
  }
317
- logStep('[6/9] Auth codemod', 'running migrate-auth-v5');
348
+ logStep('[6/10] Auth codemod', 'running migrate-auth-v5');
318
349
  try {
319
350
  const prevArgv = process.argv;
320
351
  process.argv = [
@@ -334,21 +365,21 @@ function stepAuthCodemod(cwd, flags) {
334
365
  return result.then(
335
366
  () => {
336
367
  finish();
337
- logStep('[6/9] Auth codemod', 'ok');
368
+ logStep('[6/10] Auth codemod', 'ok');
338
369
  return { ok: true };
339
370
  },
340
371
  (err) => {
341
372
  finish();
342
- logStep('[6/9] Auth codemod', `FAILED: ${err.message}`);
373
+ logStep('[6/10] Auth codemod', `FAILED: ${err.message}`);
343
374
  return { ok: false };
344
375
  }
345
376
  );
346
377
  }
347
378
  finish();
348
- logStep('[6/9] Auth codemod', 'ok');
379
+ logStep('[6/10] Auth codemod', 'ok');
349
380
  return { ok: true };
350
381
  } catch (err) {
351
- logStep('[6/9] Auth codemod', `FAILED: ${err.message}`);
382
+ logStep('[6/10] Auth codemod', `FAILED: ${err.message}`);
352
383
  return { ok: false };
353
384
  }
354
385
  }
@@ -356,38 +387,65 @@ function stepAuthCodemod(cwd, flags) {
356
387
  function stepSentryCodemod(cwd, flags, hasSentry) {
357
388
  if (flags.skipSentry || !hasSentry) {
358
389
  const reason = flags.skipSentry ? 'skipped' : 'no @sentry/nextjs, skipping';
359
- logStep('[7/9] Sentry codemod', reason);
390
+ logStep('[7/10] Sentry codemod', reason);
360
391
  return { ok: true };
361
392
  }
362
393
  if (flags.dryRun) {
363
- logStep('[7/9] Sentry codemod', '[DRY] would run sentry-9');
394
+ logStep('[7/10] Sentry codemod', '[DRY] would run sentry-9');
364
395
  return { ok: true };
365
396
  }
366
- logStep('[7/9] Sentry codemod', 'running sentry-9');
397
+ logStep('[7/10] Sentry codemod', 'running sentry-9');
367
398
  try {
368
399
  const savedCwd = process.cwd();
369
400
  process.chdir(cwd);
370
401
  const codemod = require(SENTRY_CODEMOD);
371
402
  codemod.transform();
372
403
  process.chdir(savedCwd);
373
- logStep('[7/9] Sentry codemod', 'ok');
404
+ logStep('[7/10] Sentry codemod', 'ok');
374
405
  return { ok: true };
375
406
  } catch (err) {
376
- logStep('[7/9] Sentry codemod', `FAILED: ${err.message}`);
407
+ logStep('[7/10] Sentry codemod', `FAILED: ${err.message}`);
408
+ return { ok: false };
409
+ }
410
+ }
411
+
412
+ function stepEslintCodemod(cwd, flags) {
413
+ if (flags.skipEslint) {
414
+ logStep('[8/10] ESLint codemod', 'skipped');
415
+ return { ok: true };
416
+ }
417
+ logStep('[8/10] ESLint codemod', 'running migrate-eslint');
418
+ try {
419
+ const prevArgv = process.argv;
420
+ process.argv = [
421
+ process.argv[0],
422
+ process.argv[1],
423
+ ...(flags.dryRun ? ['--dry-run'] : [])
424
+ ];
425
+ const savedCwd = process.cwd();
426
+ process.chdir(cwd);
427
+ const codemod = require(ESLINT_CODEMOD);
428
+ codemod.transform();
429
+ process.chdir(savedCwd);
430
+ process.argv = prevArgv;
431
+ logStep('[8/10] ESLint codemod', 'ok');
432
+ return { ok: true };
433
+ } catch (err) {
434
+ logStep('[8/10] ESLint codemod', `FAILED: ${err.message}`);
377
435
  return { ok: false };
378
436
  }
379
437
  }
380
438
 
381
439
  function stepInstall(cwd, flags) {
382
440
  if (flags.skipInstall) {
383
- logStep('[4/9] yarn install', 'skipped');
441
+ logStep('[4/10] yarn install', 'skipped');
384
442
  return { ok: true };
385
443
  }
386
444
  if (flags.dryRun) {
387
- logStep('[4/9] yarn install', '[DRY] yarn clean && yarn');
445
+ logStep('[4/10] yarn install', '[DRY] yarn clean && yarn');
388
446
  return { ok: true };
389
447
  }
390
- logStep('[4/9] yarn install', 'running yarn clean && yarn');
448
+ logStep('[4/10] yarn install', 'running yarn clean && yarn');
391
449
  const clean = runCommand('yarn clean', {
392
450
  cwd,
393
451
  dryRun: false,
@@ -402,33 +460,33 @@ function stepInstall(cwd, flags) {
402
460
  verbose: flags.verbose
403
461
  });
404
462
  if (install.status !== 0) {
405
- logStep('[4/9] yarn install', `FAILED (exit ${install.status})`);
463
+ logStep('[4/10] yarn install', `FAILED (exit ${install.status})`);
406
464
  return { ok: false };
407
465
  }
408
- logStep('[4/9] yarn install', 'ok');
466
+ logStep('[4/10] yarn install', 'ok');
409
467
  return { ok: true };
410
468
  }
411
469
 
412
470
  function stepBuild(cwd, flags) {
413
471
  if (flags.skipBuild) {
414
- logStep('[8/9] yarn build', 'skipped');
472
+ logStep('[9/10] yarn build', 'skipped');
415
473
  return { ok: true };
416
474
  }
417
475
  if (flags.dryRun) {
418
- logStep('[8/9] yarn build', '[DRY] yarn build');
476
+ logStep('[9/10] yarn build', '[DRY] yarn build');
419
477
  return { ok: true };
420
478
  }
421
- logStep('[8/9] yarn build', 'running');
479
+ logStep('[9/10] yarn build', 'running');
422
480
  const result = runCommand('yarn build', {
423
481
  cwd,
424
482
  dryRun: false,
425
483
  verbose: flags.verbose
426
484
  });
427
485
  if (result.status !== 0) {
428
- logStep('[8/9] yarn build', `FAILED (exit ${result.status})`);
486
+ logStep('[9/10] yarn build', `FAILED (exit ${result.status})`);
429
487
  return { ok: false };
430
488
  }
431
- logStep('[8/9] yarn build', 'ok');
489
+ logStep('[9/10] yarn build', 'ok');
432
490
  return { ok: true };
433
491
  }
434
492
 
@@ -440,7 +498,7 @@ function countTodoMarkers(cwd) {
440
498
  }
441
499
 
442
500
  function stepReport(cwd, results) {
443
- logStep('[9/9] Report', 'summary');
501
+ logStep('[10/10] Report', 'summary');
444
502
  const pkg = readPackageJson(cwd) || {};
445
503
  const todoCount = countTodoMarkers(cwd);
446
504
 
@@ -452,6 +510,7 @@ function stepReport(cwd, results) {
452
510
  console.log(` Next codemod: ${results.nextCodemod ? 'ok' : 'FAILED'}`);
453
511
  console.log(` Auth codemod: ${results.authCodemod ? 'ok' : 'FAILED'}`);
454
512
  console.log(` Sentry codemod: ${results.sentryCodemod ? 'ok' : results.sentrySkippedReason || 'FAILED'}`);
513
+ console.log(` ESLint codemod: ${results.eslintCodemod ? 'ok' : 'FAILED'}`);
455
514
  console.log(` Build: ${results.build ? 'ok' : 'FAILED'}`);
456
515
  console.log('');
457
516
  console.log(` Versions after:`);
@@ -498,7 +557,7 @@ async function transform() {
498
557
  results.preflight = true;
499
558
  await maybePause(flags, 'preflight');
500
559
  } else {
501
- logStep('[1/9] Preflight', 'skipped');
560
+ logStep('[1/10] Preflight', 'skipped');
502
561
  results.preflight = true;
503
562
  }
504
563
 
@@ -529,6 +588,9 @@ async function transform() {
529
588
  else if (!hasSentry) results.sentrySkippedReason = 'no @sentry/nextjs';
530
589
  await maybePause(flags, 'sentry-codemod');
531
590
 
591
+ results.eslintCodemod = stepEslintCodemod(cwd, flags).ok;
592
+ await maybePause(flags, 'eslint-codemod');
593
+
532
594
  results.build = stepBuild(cwd, flags).ok;
533
595
  } catch (err) {
534
596
  log(`FATAL: ${err.message}`);
@@ -164,10 +164,6 @@ export default async () => {
164
164
  name: 'Tamara Payment Extension',
165
165
  value: 'pz-tamara-extension'
166
166
  },
167
- {
168
- name: 'Similar Products',
169
- value: 'pz-similar-products'
170
- },
171
167
  {
172
168
  name: 'Hepsipay',
173
169
  value: 'pz-hepsipay'
@@ -183,10 +183,6 @@ exports.default = () => __awaiter(void 0, void 0, void 0, function* () {
183
183
  name: 'Tamara Payment Extension',
184
184
  value: 'pz-tamara-extension'
185
185
  },
186
- {
187
- name: 'Similar Products',
188
- value: 'pz-similar-products'
189
- },
190
186
  {
191
187
  name: 'Hepsipay',
192
188
  value: 'pz-hepsipay'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/projectzero",
3
- "version": "2.0.10-rc.0",
3
+ "version": "2.0.10",
4
4
  "private": false,
5
5
  "description": "CLI tool to manage your Project Zero Next project",
6
6
  "bin": {
@@ -1,22 +0,0 @@
1
- 'use client';
2
-
3
- import { useSentryUncaughtErrors } from '@akinon/next/hooks';
4
-
5
- export default function GlobalError({
6
- error,
7
- reset
8
- }: {
9
- error: Error & { digest?: string };
10
- reset: () => void;
11
- }) {
12
- useSentryUncaughtErrors(error);
13
-
14
- return (
15
- <html>
16
- <body>
17
- <h2>Something went wrong!</h2>
18
- <button onClick={() => reset()}>Try again</button>
19
- </body>
20
- </html>
21
- );
22
- }