@akinon/next 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @akinon/next
2
2
 
3
+ ## 1.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - db017b2c: ZERO-2271: Fix showing currency code instead of label
8
+ - db017b2c: ZERO-2389: Show card logo in credit card form
9
+ - db017b2c: ZERO-2387: Add sorting by date in orders page
10
+ - db017b2c: ZERO-2377: Add B2B plugin
11
+
12
+ ### Patch Changes
13
+
14
+ - db017b2c: ZERO-2417: Update basket_offers type in Product interface
15
+
16
+ ## 1.17.1
17
+
3
18
  ## 1.17.0
4
19
 
5
20
  ## 1.16.3
@@ -16,3 +16,5 @@ export * from './radio';
16
16
  export * from './react-portal';
17
17
  export * from './selected-payment-option-view';
18
18
  export * from './trans';
19
+ export * from './link';
20
+ export * from './pagination';
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ import { useLocalization } from '@akinon/next/hooks';
4
+ import { LocaleUrlStrategy } from '@akinon/next/localization';
5
+ import { urlLocaleMatcherRegex } from '@akinon/next/utils';
6
+ import NextLink, { LinkProps as NextLinkProps } from 'next/link';
7
+ import { useEffect, useState } from 'react';
8
+
9
+ interface LinkProps extends NextLinkProps {
10
+ children: React.ReactNode;
11
+ className?: string;
12
+ target?: '_blank' | '_self' | '_parent' | '_top';
13
+ }
14
+
15
+ export const Link = ({
16
+ children,
17
+ target,
18
+ className,
19
+ href,
20
+ ...rest
21
+ }: LinkProps) => {
22
+ const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
23
+ const [formattedHref, setFormattedHref] = useState(href ?? '#');
24
+
25
+ useEffect(() => {
26
+ if (typeof href !== 'string') return;
27
+ if (href.startsWith('http')) return;
28
+
29
+ const pathnameWithoutLocale = href.replace(urlLocaleMatcherRegex, '');
30
+ const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
31
+
32
+ if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
33
+ setFormattedHref(hrefWithLocale);
34
+ } else if (
35
+ localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
36
+ locale !== defaultLocaleValue
37
+ ) {
38
+ setFormattedHref(hrefWithLocale);
39
+ }
40
+ }, [href, defaultLocaleValue, locale, localeUrlStrategy]);
41
+
42
+ return (
43
+ <NextLink
44
+ href={formattedHref}
45
+ target={target}
46
+ className={className}
47
+ {...rest}
48
+ >
49
+ {children}
50
+ </NextLink>
51
+ );
52
+ };
@@ -0,0 +1,194 @@
1
+ 'use client';
2
+
3
+ import { MouseEvent, useCallback, useEffect, useState } from 'react';
4
+ import { PaginationProps } from '@theme/components/types';
5
+ import { twMerge } from 'tailwind-merge';
6
+ import clsx from 'clsx';
7
+
8
+ import usePagination from '@akinon/next/hooks/use-pagination';
9
+ import { useLocalization } from '@akinon/next/hooks';
10
+ import { useRouter } from '@akinon/next/hooks';
11
+ import { Link } from './link';
12
+
13
+ export const Pagination = (props: PaginationProps) => {
14
+ const { t } = useLocalization();
15
+ const router = useRouter();
16
+ const {
17
+ total,
18
+ limit,
19
+ currentPage,
20
+ numberOfPages,
21
+ containerClassName,
22
+ prevClassName,
23
+ pageClassName,
24
+ nextClassName,
25
+ threshold = 1,
26
+ render
27
+ } = props;
28
+
29
+ const pagination = usePagination(total, limit, currentPage, numberOfPages);
30
+ const {
31
+ total: paginationTotal,
32
+ limit: paginationLimit,
33
+ page,
34
+ pageList,
35
+ prev,
36
+ next,
37
+ setTotal,
38
+ setLimit
39
+ } = pagination;
40
+
41
+ const [paginationItems, setPaginationItems] = useState([]);
42
+ const showNext = currentPage * paginationLimit < total;
43
+
44
+ const createListItems = useCallback(() => {
45
+ setPaginationItems([]);
46
+ const delta = 2;
47
+ const startPage = Math.max(Number(page) - delta, 1);
48
+ const endPage = Math.min(Number(page) + delta, numberOfPages);
49
+
50
+ setPaginationItems((prev) => [
51
+ ...prev,
52
+ {
53
+ page: pageList[0].page,
54
+ url: pageList[0].url
55
+ }
56
+ ]);
57
+
58
+ if (delta < startPage) {
59
+ setPaginationItems((prev) => [...prev, { page: '...', url: '#' }]);
60
+ }
61
+
62
+ for (let i = startPage; i <= endPage; i++) {
63
+ if (i === 1) continue;
64
+
65
+ setPaginationItems((prev) => [
66
+ ...prev,
67
+ {
68
+ page: pageList[i - 1]?.page,
69
+ url: pageList[i - 1]?.url
70
+ }
71
+ ]);
72
+ }
73
+
74
+ if (endPage < numberOfPages - threshold) {
75
+ setPaginationItems((prev) => [...prev, { page: '...', url: '#' }]);
76
+ }
77
+
78
+ if (page < numberOfPages - delta) {
79
+ setPaginationItems((prev) => [
80
+ ...prev,
81
+ {
82
+ page: pageList[pageList.length - threshold].page,
83
+ url: pageList[pageList.length - threshold].url
84
+ }
85
+ ]);
86
+ }
87
+ }, [numberOfPages, page, pageList, threshold]);
88
+
89
+ const handleClick = (e: MouseEvent<HTMLAnchorElement>, url: string) => {
90
+ e.preventDefault();
91
+
92
+ const newUrl = new URL(url, window.location.origin);
93
+ const page = newUrl.searchParams.get('page');
94
+
95
+ if (page === '1') {
96
+ newUrl.searchParams.delete('page');
97
+ }
98
+
99
+ router.push(newUrl.pathname + newUrl.search, undefined);
100
+ };
101
+
102
+ useEffect(() => {
103
+ createListItems();
104
+ }, [createListItems, page]);
105
+
106
+ useEffect(() => {
107
+ if (total && total !== paginationTotal) {
108
+ setTotal(total);
109
+ }
110
+ }, [total, paginationTotal, setTotal]);
111
+
112
+ useEffect(() => {
113
+ if (limit && limit !== paginationLimit) {
114
+ setLimit(limit);
115
+ }
116
+ }, [limit, paginationLimit, setLimit]);
117
+
118
+ if (render) {
119
+ return <>{render(pagination)}</>;
120
+ }
121
+
122
+ return (
123
+ <ul
124
+ className={twMerge(
125
+ 'flex mt-8 mb-4 justify-center items-center',
126
+ containerClassName
127
+ )}
128
+ >
129
+ {prev && (
130
+ <li>
131
+ <Link
132
+ onClick={(e) => handleClick(e, prev)}
133
+ href={prev}
134
+ className={twMerge(
135
+ 'flex cursor-pointer text-sm px-2',
136
+ prevClassName
137
+ )}
138
+ >
139
+ <span>&lt;</span>
140
+ <span className="hidden lg:inline-block ms-4">
141
+ {t('category.pagination.previous')}
142
+ </span>
143
+ </Link>
144
+ </li>
145
+ )}
146
+
147
+ {paginationItems &&
148
+ paginationItems?.map((item, i) => (
149
+ <li key={i}>
150
+ {item?.url != '#' ? (
151
+ <Link
152
+ onClick={(e) => handleClick(e, item.url)}
153
+ href={item.url}
154
+ className={twMerge(
155
+ clsx(
156
+ 'text-xs px-2 cursor-pointer',
157
+ { 'pointer-events-none': item.url === null },
158
+ Number(page) === Number(item?.page)
159
+ ? 'font-semibold text-black-800'
160
+ : 'text-gray-400'
161
+ ),
162
+ pageClassName
163
+ )}
164
+ >
165
+ {item?.page}
166
+ </Link>
167
+ ) : (
168
+ <span className="cursor-default text-xs flex items-center justify-center">
169
+ {item?.page}
170
+ </span>
171
+ )}
172
+ </li>
173
+ ))}
174
+
175
+ {showNext && (
176
+ <li>
177
+ <Link
178
+ onClick={(e) => handleClick(e, next)}
179
+ href={next}
180
+ className={twMerge(
181
+ 'flex cursor-pointer text-xs px-2',
182
+ nextClassName
183
+ )}
184
+ >
185
+ <span className="hidden lg:inline-block me-4">
186
+ {t('category.pagination.next')}
187
+ </span>
188
+ <span>&gt;</span>
189
+ </Link>
190
+ </li>
191
+ )}
192
+ </ul>
193
+ );
194
+ };
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import dynamic from 'next/dynamic';
2
4
  import plugins from 'plugins';
3
5
  import logger from '../utils/log';
@@ -12,7 +14,8 @@ enum Plugin {
12
14
  GPay = 'pz-gpay',
13
15
  Otp = 'pz-otp',
14
16
  BKMExpress = 'pz-bkm',
15
- CreditPayment = 'pz-credit-payment'
17
+ CreditPayment = 'pz-credit-payment',
18
+ B2B = 'pz-b2b'
16
19
  }
17
20
 
18
21
  export enum Component {
@@ -24,7 +27,9 @@ export enum Component {
24
27
  GPay = 'GPayOption',
25
28
  Otp = 'Otp',
26
29
  BKMExpress = 'BKMOption',
27
- CreditPayment = 'CreditPayment'
30
+ CreditPayment = 'CreditPayment',
31
+ MyQuotationsB2B = 'AccountMyQuotations',
32
+ BasketB2B = 'BasketB2b'
28
33
  }
29
34
 
30
35
  const PluginComponents = new Map([
@@ -36,7 +41,8 @@ const PluginComponents = new Map([
36
41
  [Plugin.GPay, [Component.GPay]],
37
42
  [Plugin.Otp, [Component.Otp]],
38
43
  [Plugin.BKMExpress, [Component.BKMExpress]],
39
- [Plugin.CreditPayment, [Component.CreditPayment]]
44
+ [Plugin.CreditPayment, [Component.CreditPayment]],
45
+ [Plugin.B2B, [Component.MyQuotationsB2B, Component.BasketB2B]],
40
46
  ]);
41
47
 
42
48
  const getPlugin = (component: Component) => {
@@ -81,6 +87,8 @@ export default function PluginModule({
81
87
  promise = import(`${'@akinon/pz-bkm'}`);
82
88
  } else if (plugin === Plugin.CreditPayment) {
83
89
  promise = import(`${'@akinon/pz-credit-payment'}`);
90
+ } else if (plugin === Plugin.B2B) {
91
+ promise = import(`${'@akinon/pz-b2b'}`);
84
92
  }
85
93
  } catch (error) {
86
94
  logger.error(error);
@@ -20,6 +20,7 @@ interface GetOrdersParams {
20
20
  limit?: number;
21
21
  page?: number;
22
22
  createdDate?: string;
23
+ endDate?: string;
23
24
  }
24
25
 
25
26
  export interface GetQuotationsResponse {
@@ -105,8 +106,10 @@ const accountApi = api.injectEndpoints({
105
106
  query: (id) => buildClientRequestUrl(account.orderId(id))
106
107
  }),
107
108
  getOrders: builder.query<GetOrdersResponse, GetOrdersParams>({
108
- query: ({ page, limit, createdDate } = {}) =>
109
- buildClientRequestUrl(account.getOrders({ page, limit, createdDate }))
109
+ query: ({ page, limit, createdDate, endDate } = {}) =>
110
+ buildClientRequestUrl(
111
+ account.getOrders({ page, limit, createdDate, endDate })
112
+ )
110
113
  }),
111
114
  getQuotations: builder.query<GetQuotationsResponse, void>({
112
115
  query: () => buildClientRequestUrl(account.getQuotations)
@@ -4,7 +4,8 @@ import {
4
4
  setPaymentStepBusy,
5
5
  setSelectedBankAccountPk,
6
6
  setSelectedCreditPaymentPk,
7
- setShippingStepBusy
7
+ setShippingStepBusy,
8
+ setCardType
8
9
  } from '../../redux/reducers/checkout';
9
10
  import {
10
11
  CheckoutContext,
@@ -231,6 +232,7 @@ export const checkoutApi = api.injectEndpoints({
231
232
  dispatch(setInstallmentOptions([]));
232
233
  dispatch(setBankAccounts([]));
233
234
  dispatch(setSelectedBankAccountPk(null));
235
+ dispatch(setCardType(null));
234
236
  await queryFulfilled;
235
237
  dispatch(setPaymentStepBusy(false));
236
238
  }
@@ -256,6 +258,7 @@ export const checkoutApi = api.injectEndpoints({
256
258
  },
257
259
  async onQueryStarted(arg, { dispatch, queryFulfilled }) {
258
260
  dispatch(setPaymentStepBusy(true));
261
+ dispatch(setCardType(arg));
259
262
  await queryFulfilled;
260
263
  dispatch(setPaymentStepBusy(false));
261
264
  }
package/data/urls.ts CHANGED
@@ -16,15 +16,17 @@ export const account = {
16
16
  getOrders: ({
17
17
  page,
18
18
  limit,
19
- createdDate
19
+ createdDate,
20
+ endDate
20
21
  }: {
21
22
  page?: number;
22
23
  limit?: number;
23
24
  createdDate?: string;
25
+ endDate?: string;
24
26
  }) =>
25
27
  `/users/orders/?page=${page || 1}&limit=${limit || 12} ${
26
28
  createdDate ? `&created_date__gte=${createdDate}` : ''
27
- }`,
29
+ } ${endDate ? `&created_date__lte=${endDate}` : ''}`,
28
30
  getQuotations: '/b2b/my-quotations',
29
31
  sendContact: '/users/contact-us',
30
32
  passwordReset: (slug: string) => `/users/reset/${slug}`,
package/hooks/index.ts CHANGED
@@ -8,3 +8,4 @@ export * from './use-media-query';
8
8
  export * from './use-on-click-outside';
9
9
  export * from './use-mobile-iframe-handler';
10
10
  export * from './use-payment-options';
11
+ export * from './use-pagination';
@@ -0,0 +1,136 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useMemo, useReducer } from 'react';
4
+ import { usePathname, useSearchParams } from 'next/navigation';
5
+
6
+ export type UsePaginationType = ReturnType<typeof usePagination>;
7
+
8
+ type InitialState = {
9
+ page: number;
10
+ last: number;
11
+ limit: number;
12
+ total: number;
13
+ };
14
+
15
+ type ACTIONTYPE =
16
+ | { type: 'setPage'; payload: number }
17
+ | { type: 'setTotal'; payload: number }
18
+ | { type: 'setLimit'; payload: number };
19
+
20
+ function reducer(state: InitialState, action: ACTIONTYPE) {
21
+ switch (action.type) {
22
+ case 'setTotal':
23
+ return {
24
+ ...state,
25
+ total: action.payload,
26
+ last: Math.ceil(action.payload / state.limit)
27
+ };
28
+ case 'setPage':
29
+ return { ...state, page: action.payload };
30
+ case 'setLimit':
31
+ return { ...state, limit: action.payload };
32
+ default:
33
+ throw new Error();
34
+ }
35
+ }
36
+
37
+ export default function usePagination(
38
+ _total = 0,
39
+ _limit = 12,
40
+ _page: number | undefined,
41
+ _last: number | undefined
42
+ ) {
43
+ const pathname = usePathname();
44
+ const searchParams = useSearchParams();
45
+ const urlSearchParams = useMemo(
46
+ () => new URLSearchParams(searchParams.toString()),
47
+ [searchParams]
48
+ );
49
+ const { page, limit } = useMemo(
50
+ () => ({
51
+ page: _page || Number(searchParams.get('page')) || 1,
52
+ limit: _limit || Number(searchParams.get('limit'))
53
+ }),
54
+ [searchParams, _page, _limit]
55
+ );
56
+
57
+ const initialState: InitialState = {
58
+ page,
59
+ limit,
60
+ last: _last || Math.ceil(_total / limit) || 1,
61
+ total: _total
62
+ };
63
+ const [state, dispatch] = useReducer(reducer, initialState);
64
+
65
+ useEffect(() => {
66
+ dispatch({ type: 'setPage', payload: page });
67
+ }, [page]);
68
+
69
+ useEffect(() => {
70
+ dispatch({ type: 'setLimit', payload: limit });
71
+ }, [limit]);
72
+
73
+ useEffect(() => {
74
+ window.scrollTo(0, 0);
75
+ }, [state.page, state.limit]);
76
+
77
+ const setTotal = useCallback(
78
+ (total: number) => {
79
+ dispatch({ type: 'setTotal', payload: total });
80
+ },
81
+ [dispatch]
82
+ );
83
+
84
+ const setPage = useCallback(
85
+ (page: number) => {
86
+ if (page > 0 && page <= state.total) {
87
+ dispatch({ type: 'setPage', payload: page });
88
+ }
89
+ },
90
+ [dispatch, state.total]
91
+ );
92
+
93
+ const setLimit = useCallback(
94
+ (limit: number) => {
95
+ dispatch({ type: 'setLimit', payload: limit });
96
+ },
97
+ [dispatch]
98
+ );
99
+
100
+ const pageList = useMemo(() => {
101
+ return Array.from({ length: state.last }, (_, i) => {
102
+ urlSearchParams.set('page', (i + 1).toString());
103
+
104
+ return {
105
+ page: i + 1,
106
+ url: `${pathname}?${urlSearchParams.toString()}`
107
+ };
108
+ });
109
+ }, [state.last, pathname, urlSearchParams]);
110
+
111
+ const prev = useMemo(() => {
112
+ if (state.page > 1) {
113
+ urlSearchParams.set('page', (Number(state.page) - 1).toString());
114
+ return `${pathname}?${urlSearchParams.toString()}`;
115
+ }
116
+ return null;
117
+ }, [state.page, pathname, urlSearchParams]);
118
+
119
+ const next = useMemo(() => {
120
+ if (Number(state.page) < Number(state.last)) {
121
+ urlSearchParams.set('page', (Number(state.page) + 1).toString());
122
+ return `${pathname}?${urlSearchParams.toString()}`;
123
+ }
124
+ return null;
125
+ }, [state.page, state.last, pathname, urlSearchParams]);
126
+
127
+ return {
128
+ ...state,
129
+ setTotal,
130
+ setPage,
131
+ setLimit,
132
+ pageList,
133
+ prev,
134
+ next
135
+ };
136
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@akinon/next",
3
3
  "description": "Core package for Project Zero Next",
4
- "version": "1.17.0",
4
+ "version": "1.18.0",
5
5
  "private": false,
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "@typescript-eslint/eslint-plugin": "6.7.4",
32
32
  "@typescript-eslint/parser": "6.7.4",
33
33
  "eslint": "^8.14.0",
34
- "@akinon/eslint-plugin-projectzero": "1.17.0",
34
+ "@akinon/eslint-plugin-projectzero": "1.18.0",
35
35
  "eslint-config-prettier": "8.5.0"
36
36
  }
37
37
  }
package/plugins.js CHANGED
@@ -8,4 +8,5 @@ module.exports = [
8
8
  'pz-otp',
9
9
  'pz-bkm',
10
10
  'pz-credit-payment',
11
+ 'pz-b2b'
11
12
  ];
@@ -10,14 +10,14 @@ export interface Product {
10
10
  };
11
11
  attributes_kwargs: any;
12
12
  base_code: string;
13
- basket_offers: {
13
+ basket_offers: Array<{
14
14
  kwargs: { show_benefit_products: boolean };
15
15
  label: string;
16
16
  listing_kwargs: {
17
17
  [key: string]: any;
18
18
  };
19
19
  pk: number;
20
- };
20
+ }>;
21
21
  currency_type: string;
22
22
  data_source: null;
23
23
  extra_attributes: {
@@ -0,0 +1,9 @@
1
+ import settings from 'settings';
2
+
3
+ export const getCurrencyLabel = (currencyCode: string) => {
4
+ const currencyLabel = settings.localization.currencies.find(
5
+ (item) => item.code === currencyCode
6
+ )?.label;
7
+
8
+ return currencyLabel ?? currencyCode;
9
+ };
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import getSymbolFromCurrency from 'currency-symbol-map';
4
+ import { getCurrencyLabel } from './get-currency-label';
4
5
 
5
6
  type GetCurrencyType = {
6
7
  currencyCode: string;
@@ -17,10 +18,11 @@ export const getCurrency = (args: GetCurrencyType) => {
17
18
  useCurrencySpace = true
18
19
  } = args;
19
20
 
21
+ const currencyLabel = getCurrencyLabel(currencyCode);
20
22
  const currencySpace = useCurrencySpace ? ' ' : '';
21
23
  const currency = useCurrencySymbol
22
- ? `${getSymbolFromCurrency(currencyCode)}`
23
- : currencyCode;
24
+ ? `${getSymbolFromCurrency(currencyLabel)}`
25
+ : currencyLabel;
24
26
  const currencyAfterPrice = useCurrencyAfterPrice
25
27
  ? `${currencySpace}${currency}`
26
28
  : `${currency}${currencySpace}`;
package/utils/index.ts CHANGED
@@ -5,6 +5,7 @@ import { CDNOptions, ClientRequestOptions } from '../types';
5
5
  export * from './get-currency';
6
6
  export * from './menu-generator';
7
7
  export * from './generate-commerce-search-params';
8
+ export * from './get-currency-label';
8
9
 
9
10
  export function getCookie(name: string) {
10
11
  if (typeof document === 'undefined') {