@akinon/projectzero 2.0.32 → 2.0.33-rc.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/app-template/.env.example +1 -0
  3. package/app-template/AGENTS.md +8 -0
  4. package/app-template/CHANGELOG.md +77 -0
  5. package/app-template/README.md +25 -1
  6. package/app-template/next.config.mjs +7 -1
  7. package/app-template/package.json +41 -41
  8. package/app-template/public/locales/en/checkout.json +50 -0
  9. package/app-template/public/locales/tr/checkout.json +50 -0
  10. package/app-template/src/app/[pz]/basket/page.tsx +33 -6
  11. package/app-template/src/app/[pz]/orders/checkout/page.tsx +57 -92
  12. package/app-template/src/app/api/theme-settings/route.ts +21 -2
  13. package/app-template/src/app/global-error.tsx +22 -0
  14. package/app-template/src/data/server/theme.ts +6 -25
  15. package/app-template/src/hooks/index.ts +2 -0
  16. package/app-template/src/plugins.js +1 -0
  17. package/app-template/src/proxy.ts +2 -1
  18. package/app-template/src/views/checkout/variants/basket-and-checkout/basket-section.tsx +149 -0
  19. package/app-template/src/views/checkout/variants/basket-and-checkout/index.tsx +132 -0
  20. package/app-template/src/views/checkout/variants/multi-step/index.tsx +73 -0
  21. package/app-template/src/views/checkout/variants/one-page/accordion-section.tsx +140 -0
  22. package/app-template/src/views/checkout/variants/one-page/hooks/use-checkout-funnel-gtm.ts +60 -0
  23. package/app-template/src/views/checkout/variants/one-page/hooks/use-section-state.ts +99 -0
  24. package/app-template/src/views/checkout/variants/one-page/index.tsx +125 -0
  25. package/app-template/src/views/checkout/variants/one-page/mobile-summary-sheet.tsx +109 -0
  26. package/app-template/src/views/checkout/variants/one-page/one-page-summary.tsx +237 -0
  27. package/app-template/src/views/checkout/variants/one-page/payment-options-grid.tsx +201 -0
  28. package/app-template/src/views/checkout/variants/one-page/sections/address-section.tsx +318 -0
  29. package/app-template/src/views/checkout/variants/one-page/sections/contact-section.tsx +62 -0
  30. package/app-template/src/views/checkout/variants/one-page/sections/payment-section.tsx +71 -0
  31. package/app-template/src/views/checkout/variants/one-page/sections/shipping-section.tsx +298 -0
  32. package/app-template/src/views/checkout/variants/one-page/skeletons/address-skeleton.tsx +25 -0
  33. package/app-template/src/views/checkout/variants/one-page/skeletons/checkout-skeleton.tsx +27 -0
  34. package/app-template/src/views/checkout/variants/one-page/skeletons/index.ts +6 -0
  35. package/app-template/src/views/checkout/variants/one-page/skeletons/payment-skeleton.tsx +23 -0
  36. package/app-template/src/views/checkout/variants/one-page/skeletons/section-skeleton.tsx +47 -0
  37. package/app-template/src/views/checkout/variants/one-page/skeletons/shipping-skeleton.tsx +20 -0
  38. package/app-template/src/views/checkout/variants/one-page/skeletons/summary-skeleton.tsx +38 -0
  39. package/app-template/src/views/checkout/variants/one-page/trust-badges.tsx +30 -0
  40. package/app-template/src/views/checkout/variants/registry.ts +32 -0
  41. package/app-template/src/views/checkout/variants/types.ts +30 -0
  42. package/app-template/src/views/checkout/variants/use-resolved-config.ts +89 -0
  43. package/app-template/src/views/header/search/index.tsx +13 -5
  44. package/app-template/src/views/product/slider.tsx +85 -38
  45. package/commands/plugins.ts +4 -0
  46. package/dist/commands/plugins.js +4 -0
  47. package/package.json +1 -1
@@ -0,0 +1,237 @@
1
+ import { useAppSelector } from '@akinon/next/redux/hooks';
2
+ import { useLocalization } from '@akinon/next/hooks';
3
+ import PluginModule, { Component } from '@akinon/next/components/plugin-module';
4
+ import { Price, Link } from '@theme/components';
5
+ import { Image } from '@akinon/next/components/image';
6
+ import { StoreCredits } from '@theme/views/checkout/steps/payment/options/store-credit';
7
+ import type { RootState } from '@theme/redux/store';
8
+ import type { SectionId } from './hooks/use-section-state';
9
+
10
+ interface OnePageSummaryProps {
11
+ onEditSection: (section: SectionId) => void;
12
+ showReview?: boolean;
13
+ }
14
+
15
+ const OnePageSummary = ({
16
+ onEditSection,
17
+ showReview = true
18
+ }: OnePageSummaryProps) => {
19
+ const { t } = useLocalization();
20
+ const preOrder = useAppSelector(
21
+ (state: RootState) => state.checkout.preOrder
22
+ );
23
+ const { isMobileApp } = useAppSelector((state: RootState) => state.root);
24
+
25
+ if (!preOrder?.basket) return null;
26
+
27
+ const itemCount = preOrder.basket.basketitem_set.length;
28
+
29
+ return (
30
+ <div className="flex flex-col gap-4">
31
+ <PluginModule
32
+ component={Component.CheckoutGiftPack}
33
+ props={{
34
+ className:
35
+ 'flex flex-col w-full rounded-2xl bg-white shadow-[0_1px_3px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.04]'
36
+ }}
37
+ />
38
+ <StoreCredits />
39
+
40
+ <section className="overflow-hidden rounded-2xl bg-white shadow-[0_1px_3px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.04]">
41
+ <header className="flex items-baseline justify-between px-6 py-5">
42
+ <span className="text-lg font-semibold tracking-tight text-black-800">
43
+ {t('checkout.summary.title')}
44
+ </span>
45
+ <span className="text-[11px] font-medium uppercase tracking-wider text-gray-500">
46
+ {itemCount} {t('checkout.summary.items')}
47
+ </span>
48
+ </header>
49
+
50
+ <ul className="max-h-72 divide-y divide-gray-100 overflow-y-auto border-t border-gray-100">
51
+ {preOrder.basket.basketitem_set.map((item, index) => (
52
+ <li
53
+ key={`one-page-summary-item-${index}`}
54
+ className="flex gap-3 px-6 py-4"
55
+ >
56
+ <Link
57
+ href={item.product.absolute_url || '#'}
58
+ passHref
59
+ className={
60
+ isMobileApp
61
+ ? 'pointer-events-none flex-shrink-0'
62
+ : 'flex-shrink-0 overflow-hidden rounded-md'
63
+ }
64
+ >
65
+ <Image
66
+ src={item.product.productimage_set[0]?.image}
67
+ alt={item.product.name}
68
+ width={56}
69
+ height={84}
70
+ />
71
+ </Link>
72
+ <div className="flex flex-1 flex-col justify-between text-xs">
73
+ <Link
74
+ href={item.product.absolute_url || '#'}
75
+ className="line-clamp-2 text-sm font-medium text-black-800 transition-colors hover:text-secondary"
76
+ >
77
+ {item.product.name}
78
+ </Link>
79
+ <div className="flex items-end justify-between gap-2">
80
+ <span className="text-gray-500">
81
+ {t('checkout.summary.quantity')}:{' '}
82
+ <span className="font-medium tabular-nums text-black-800">
83
+ {item.quantity}
84
+ </span>
85
+ </span>
86
+ <div className="flex flex-col items-end gap-0.5">
87
+ {item.product.retail_price !== item.product.price && (
88
+ <span className="text-[10px] text-gray-400 line-through">
89
+ <Price value={item.product.retail_price} />
90
+ </span>
91
+ )}
92
+ <span className="text-sm font-semibold text-black-800 tabular-nums">
93
+ <Price value={item.product.price} />
94
+ </span>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </li>
99
+ ))}
100
+ </ul>
101
+
102
+ <dl className="space-y-2.5 border-t border-gray-100 px-6 py-5 text-sm text-gray-600">
103
+ <div className="flex items-center justify-between">
104
+ <dt>{t('checkout.summary.subtotal')}</dt>
105
+ <dd className="font-medium tabular-nums text-black-800">
106
+ <Price value={preOrder.basket.total_amount} />
107
+ </dd>
108
+ </div>
109
+ <div className="flex items-center justify-between">
110
+ <dt>{t('checkout.summary.shipping')}</dt>
111
+ <dd className="font-medium tabular-nums text-black-800">
112
+ <Price value={preOrder.shipping_amount} />
113
+ </dd>
114
+ </div>
115
+ {parseFloat(preOrder.loyalty_money || '0') > 0 && (
116
+ <div className="flex items-center justify-between">
117
+ <dt>{t('checkout.summary.loyalty_money_total')}</dt>
118
+ <dd className="font-medium tabular-nums text-black-800">
119
+ <Price value={preOrder.loyalty_money} />
120
+ </dd>
121
+ </div>
122
+ )}
123
+ {parseFloat(preOrder.basket.total_discount_amount || '0') > 0 && (
124
+ <div className="flex items-center justify-between">
125
+ <dt>{t('checkout.summary.discounts_total')}</dt>
126
+ <dd className="font-medium tabular-nums text-black-800">
127
+ <Price
128
+ value={preOrder.basket.total_discount_amount}
129
+ useNegative
130
+ />
131
+ </dd>
132
+ </div>
133
+ )}
134
+ </dl>
135
+
136
+ <div className="flex items-baseline justify-between border-t border-gray-100 px-6 py-5">
137
+ <span className="text-sm font-medium uppercase tracking-wider text-gray-700">
138
+ {t('checkout.summary.total')}
139
+ </span>
140
+ <span className="text-2xl font-bold tracking-tight text-black-800 tabular-nums">
141
+ <Price value={preOrder.unpaid_amount} />
142
+ </span>
143
+ </div>
144
+ </section>
145
+
146
+ {showReview &&
147
+ (preOrder.shipping_address ||
148
+ preOrder.shipping_option ||
149
+ preOrder.payment_option) && (
150
+ <section className="overflow-hidden rounded-2xl bg-white shadow-[0_1px_3px_rgba(0,0,0,0.04)] ring-1 ring-black/[0.04]">
151
+ <header className="px-6 py-5">
152
+ <span className="text-base font-semibold tracking-tight text-black-800">
153
+ {t('checkout.one_page.summary.review_title')}
154
+ </span>
155
+ </header>
156
+
157
+ <ul className="divide-y divide-gray-100 border-t border-gray-100 text-sm text-black-800">
158
+ {preOrder.shipping_address && (
159
+ <li className="flex items-start justify-between gap-3 px-6 py-4">
160
+ <div className="flex-1">
161
+ <div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
162
+ {t('checkout.one_page.summary.deliver_to')}
163
+ </div>
164
+ <div className="font-medium">
165
+ {preOrder.shipping_address.title}
166
+ </div>
167
+ <div className="text-xs text-gray-600">
168
+ {preOrder.shipping_address.line},{' '}
169
+ {preOrder.shipping_address.township?.name},{' '}
170
+ {preOrder.shipping_address.city?.name}
171
+ </div>
172
+ </div>
173
+ <button
174
+ type="button"
175
+ onClick={() => onEditSection('address')}
176
+ className="rounded-md px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-black-800"
177
+ data-testid="one-page-summary-edit-address"
178
+ >
179
+ {t('checkout.one_page.edit')}
180
+ </button>
181
+ </li>
182
+ )}
183
+
184
+ {preOrder.shipping_option && (
185
+ <li className="flex items-start justify-between gap-3 px-6 py-4">
186
+ <div className="flex-1">
187
+ <div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
188
+ {t('checkout.one_page.summary.shipping_method')}
189
+ </div>
190
+ <div className="font-medium">
191
+ {preOrder.shipping_option.name}
192
+ </div>
193
+ {preOrder.shipping_option.shipping_amount && (
194
+ <div className="text-xs tabular-nums text-gray-600">
195
+ <Price value={preOrder.shipping_option.shipping_amount} />
196
+ </div>
197
+ )}
198
+ </div>
199
+ <button
200
+ type="button"
201
+ onClick={() => onEditSection('shipping')}
202
+ className="rounded-md px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-black-800"
203
+ data-testid="one-page-summary-edit-shipping"
204
+ >
205
+ {t('checkout.one_page.edit')}
206
+ </button>
207
+ </li>
208
+ )}
209
+
210
+ {preOrder.payment_option && (
211
+ <li className="flex items-start justify-between gap-3 px-6 py-4">
212
+ <div className="flex-1">
213
+ <div className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
214
+ {t('checkout.one_page.summary.payment_method')}
215
+ </div>
216
+ <div className="font-medium">
217
+ {preOrder.payment_option.name}
218
+ </div>
219
+ </div>
220
+ <button
221
+ type="button"
222
+ onClick={() => onEditSection('payment')}
223
+ className="rounded-md px-2 py-1 text-xs font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-black-800"
224
+ data-testid="one-page-summary-edit-payment"
225
+ >
226
+ {t('checkout.one_page.edit')}
227
+ </button>
228
+ </li>
229
+ )}
230
+ </ul>
231
+ </section>
232
+ )}
233
+ </div>
234
+ );
235
+ };
236
+
237
+ export default OnePageSummary;
@@ -0,0 +1,201 @@
1
+ import { useCallback, useMemo, useRef } from 'react';
2
+ import clsx from 'clsx';
3
+ import { useAppSelector } from '@akinon/next/redux/hooks';
4
+ import { useSetPaymentOptionMutation } from '@akinon/next/data/client/checkout';
5
+ import { usePaymentOptions } from '@akinon/next/hooks/use-payment-options';
6
+ import { useLocalization } from '@akinon/next/hooks';
7
+ import { Icon } from '@theme/components';
8
+ import type { RootState } from '@theme/redux/store';
9
+ import type { CheckoutPaymentOption } from '@akinon/next/types';
10
+
11
+ const PAYMENT_TYPE_ICON: Record<string, string> = {
12
+ credit_card: 'cvc',
13
+ funds_transfer: 'file',
14
+ pay_on_delivery: 'address',
15
+ bkm_express: 'akinon',
16
+ masterpass: 'cvc',
17
+ masterpass_rest: 'cvc',
18
+ saved_card: 'cvc',
19
+ gpay: 'google',
20
+ google_pay: 'google',
21
+ apple_pay: 'apple',
22
+ wallet: 'akinon',
23
+ redirection: 'globe',
24
+ credit_payment: 'installment',
25
+ loyalty: 'heart-full',
26
+ hepsipay: 'akinon',
27
+ tabby: 'installment',
28
+ tamara: 'installment',
29
+ iyzico: 'cvc',
30
+ cybersource_uc: 'cvc',
31
+ flow_payment: 'installment'
32
+ };
33
+
34
+ const SLUG_KEYWORD_ICON: Array<[string, string]> = [
35
+ ['apple', 'apple'],
36
+ ['google', 'google'],
37
+ ['gpay', 'google'],
38
+ ['hepsi', 'akinon'],
39
+ ['tabby', 'installment'],
40
+ ['tamara', 'installment'],
41
+ ['flow', 'installment'],
42
+ ['saved', 'cvc'],
43
+ ['credit', 'cvc'],
44
+ ['transfer', 'file'],
45
+ ['delivery', 'address'],
46
+ ['wallet', 'akinon']
47
+ ];
48
+
49
+ const getOptionIcon = (option: CheckoutPaymentOption): string => {
50
+ if (PAYMENT_TYPE_ICON[option.payment_type]) {
51
+ return PAYMENT_TYPE_ICON[option.payment_type];
52
+ }
53
+ const slug = option.slug?.toLowerCase() || '';
54
+ for (const [keyword, icon] of SLUG_KEYWORD_ICON) {
55
+ if (slug.includes(keyword)) return icon;
56
+ }
57
+ return 'default';
58
+ };
59
+
60
+ const PaymentOptionsGrid = () => {
61
+ const { t } = useLocalization();
62
+ const { preOrder, attributeBasedShippingOptions } = useAppSelector(
63
+ (state: RootState) => state.checkout
64
+ );
65
+ const [setPaymentOption, { isLoading }] = useSetPaymentOptionMutation();
66
+ const { filteredPaymentOptions } = usePaymentOptions();
67
+
68
+ const containerRef = useRef<HTMLDivElement>(null);
69
+
70
+ const visibleOptions = useMemo(() => {
71
+ if (
72
+ attributeBasedShippingOptions &&
73
+ Object.keys(attributeBasedShippingOptions).length > 0
74
+ ) {
75
+ return filteredPaymentOptions.filter(
76
+ (option) => option.slug?.toLowerCase() !== 'pay-on-delivery'
77
+ );
78
+ }
79
+ return filteredPaymentOptions;
80
+ }, [filteredPaymentOptions, attributeBasedShippingOptions]);
81
+
82
+ const handleKeyDown = useCallback(
83
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
84
+ if (
85
+ !['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp', 'Home', 'End'].includes(
86
+ event.key
87
+ )
88
+ ) {
89
+ return;
90
+ }
91
+ event.preventDefault();
92
+ const buttons = Array.from(
93
+ containerRef.current?.querySelectorAll<HTMLButtonElement>(
94
+ 'button[role="radio"]'
95
+ ) ?? []
96
+ );
97
+ if (buttons.length === 0) return;
98
+ const currentIndex = buttons.findIndex(
99
+ (btn) => btn === document.activeElement
100
+ );
101
+ let nextIndex = currentIndex;
102
+ switch (event.key) {
103
+ case 'ArrowRight':
104
+ case 'ArrowDown':
105
+ nextIndex = (currentIndex + 1 + buttons.length) % buttons.length;
106
+ break;
107
+ case 'ArrowLeft':
108
+ case 'ArrowUp':
109
+ nextIndex = (currentIndex - 1 + buttons.length) % buttons.length;
110
+ break;
111
+ case 'Home':
112
+ nextIndex = 0;
113
+ break;
114
+ case 'End':
115
+ nextIndex = buttons.length - 1;
116
+ break;
117
+ }
118
+ buttons[nextIndex]?.focus();
119
+ },
120
+ []
121
+ );
122
+
123
+ if (visibleOptions.length === 0) {
124
+ return (
125
+ <p className="rounded-xl border border-dashed border-gray-200 bg-gray-50 px-5 py-4 text-sm italic text-gray-600">
126
+ {t('checkout.one_page.payment.no_options')}
127
+ </p>
128
+ );
129
+ }
130
+
131
+ return (
132
+ <div
133
+ ref={containerRef}
134
+ className={clsx(
135
+ 'grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3',
136
+ { 'pointer-events-none opacity-60': isLoading }
137
+ )}
138
+ role="radiogroup"
139
+ aria-label={t('checkout.one_page.payment.title')}
140
+ onKeyDown={handleKeyDown}
141
+ >
142
+ {visibleOptions.map((option, index) => {
143
+ const selected = preOrder?.payment_option?.pk === option.pk;
144
+ return (
145
+ <button
146
+ key={option.pk}
147
+ type="button"
148
+ role="radio"
149
+ aria-checked={selected}
150
+ tabIndex={selected || (!preOrder?.payment_option && index === 0) ? 0 : -1}
151
+ onClick={() => setPaymentOption(option.pk)}
152
+ className={clsx(
153
+ 'group relative flex min-h-[5rem] items-center gap-3',
154
+ 'rounded-xl border px-4 py-3 text-left',
155
+ 'transition-all duration-200 ease-out',
156
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-black-800 focus-visible:ring-offset-2',
157
+ selected
158
+ ? 'border-black-800 bg-black-800/[0.03] shadow-[inset_0_0_0_1px_rgb(31,31,31)]'
159
+ : 'border-gray-200 bg-white hover:border-gray-400'
160
+ )}
161
+ data-testid={`one-page-payment-card-${option.pk}`}
162
+ data-payment-type={option.payment_type}
163
+ >
164
+ <Icon
165
+ name={getOptionIcon(option)}
166
+ size={20}
167
+ className={clsx(
168
+ 'flex-shrink-0 transition-colors',
169
+ selected ? 'fill-black-800' : 'fill-gray-600'
170
+ )}
171
+ aria-hidden="true"
172
+ />
173
+ <span
174
+ className={clsx(
175
+ 'flex-1 text-sm font-medium leading-tight',
176
+ selected ? 'text-black-800' : 'text-gray-700'
177
+ )}
178
+ >
179
+ {option.name}
180
+ </span>
181
+ <span
182
+ className={clsx(
183
+ 'flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full border-2 transition-all',
184
+ selected
185
+ ? 'border-black-800 bg-black-800'
186
+ : 'border-gray-300 bg-white group-hover:border-gray-400'
187
+ )}
188
+ aria-hidden="true"
189
+ >
190
+ {selected && (
191
+ <span className="block h-1.5 w-1.5 rounded-full bg-white" />
192
+ )}
193
+ </span>
194
+ </button>
195
+ );
196
+ })}
197
+ </div>
198
+ );
199
+ };
200
+
201
+ export default PaymentOptionsGrid;