@akinon/projectzero 1.99.0-rc.66 → 1.99.0-rc.68
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
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: 'src/app/[commerce]/[locale]/[currency]/account/**'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Account Pages Development Instructions
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
Account pages in Project Zero Next.js provide e-commerce account functionality using Next.js 14 App Router structure. These instructions apply to files in the `src/app/[commerce]/[locale]/[currency]/account/` directory.
|
|
10
|
+
|
|
11
|
+
## App Router Page Structure
|
|
12
|
+
|
|
13
|
+
### Basic Page Pattern
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
// src/app/[commerce]/[locale]/[currency]/account/page.tsx
|
|
17
|
+
import { Metadata } from 'next';
|
|
18
|
+
|
|
19
|
+
export const metadata: Metadata = {
|
|
20
|
+
title: 'Account | Project Zero',
|
|
21
|
+
description: 'User account page'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
params: {
|
|
26
|
+
commerce: string;
|
|
27
|
+
locale: string;
|
|
28
|
+
currency: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function AccountPage({ params }: Props) {
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
<h1>Account Page</h1>
|
|
36
|
+
{/* Page content */}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Layout Pattern
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// src/app/[commerce]/[locale]/[currency]/account/layout.tsx
|
|
46
|
+
interface Props {
|
|
47
|
+
children: React.ReactNode;
|
|
48
|
+
params: {
|
|
49
|
+
commerce: string;
|
|
50
|
+
locale: string;
|
|
51
|
+
currency: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function AccountLayout({ children, params }: Props) {
|
|
56
|
+
return <div className="account-layout">{children}</div>;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Component Import Patterns
|
|
61
|
+
|
|
62
|
+
### Theme Components
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { Button, Input, Card } from '@theme/components';
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Akinon Next Components
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { Image } from '@akinon/next/components/image';
|
|
72
|
+
import { Link } from '@akinon/next/components/link';
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Data Fetching
|
|
76
|
+
|
|
77
|
+
### Client-Side Hooks
|
|
78
|
+
|
|
79
|
+
Hooks used for fetching account data in Project Zero:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
'use client';
|
|
83
|
+
|
|
84
|
+
import {
|
|
85
|
+
useGetProfileInfoQuery,
|
|
86
|
+
useUpdateProfileMutation,
|
|
87
|
+
useGetOrdersQuery,
|
|
88
|
+
useGetOrderQuery,
|
|
89
|
+
useGetOldOrdersQuery,
|
|
90
|
+
useGetQuotationsQuery,
|
|
91
|
+
useUpdateEmailMutation,
|
|
92
|
+
useUpdatePasswordMutation,
|
|
93
|
+
useSendContactMutation,
|
|
94
|
+
useCancelOrderMutation,
|
|
95
|
+
useBulkCancellationMutation,
|
|
96
|
+
useGetCancellationReasonsQuery,
|
|
97
|
+
useGetContactSubjectsQuery,
|
|
98
|
+
usePasswordResetMutation,
|
|
99
|
+
useGetBasketOffersQuery,
|
|
100
|
+
useGetFutureBasketOffersQuery,
|
|
101
|
+
useGetExpiredBasketOffersQuery,
|
|
102
|
+
useGetDiscountItemsQuery,
|
|
103
|
+
useAnonymizeMutation,
|
|
104
|
+
useGetLoyaltyBalanceQuery,
|
|
105
|
+
useGetLoyaltyTransactionsQuery
|
|
106
|
+
} from '@akinon/next/data/client/account';
|
|
107
|
+
|
|
108
|
+
// User profile information
|
|
109
|
+
const UserProfile = () => {
|
|
110
|
+
const { data: profile, isLoading, error } = useGetProfileInfoQuery();
|
|
111
|
+
|
|
112
|
+
if (isLoading) return <div>Loading...</div>;
|
|
113
|
+
if (error) return <div>Error occurred</div>;
|
|
114
|
+
|
|
115
|
+
return <div>Welcome, {profile?.first_name}</div>;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Orders - with pagination support
|
|
119
|
+
const OrdersList = () => {
|
|
120
|
+
const { data: orders, isLoading } = useGetOrdersQuery({
|
|
121
|
+
page: 1,
|
|
122
|
+
limit: 10
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div>
|
|
127
|
+
<h2>Orders ({orders?.count})</h2>
|
|
128
|
+
{orders?.results?.map((order) => (
|
|
129
|
+
<div key={order.id}>
|
|
130
|
+
<span>Order #{order.number}</span>
|
|
131
|
+
<span>{order.status}</span>
|
|
132
|
+
<span>{order.total} USD</span>
|
|
133
|
+
</div>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Order detail
|
|
140
|
+
const OrderDetail = ({ orderId }) => {
|
|
141
|
+
const { data: order, isLoading, error } = useGetOrderQuery(orderId);
|
|
142
|
+
|
|
143
|
+
if (isLoading) return <div>Loading order...</div>;
|
|
144
|
+
if (error) return <div>Order not found</div>;
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div>
|
|
148
|
+
<h1>Order #{order?.number}</h1>
|
|
149
|
+
<p>Status: {order?.status}</p>
|
|
150
|
+
<p>Total: {order?.total} USD</p>
|
|
151
|
+
{/* Order products and details */}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Old orders
|
|
157
|
+
const OldOrdersList = () => {
|
|
158
|
+
const { data: oldOrders, isLoading } = useGetOldOrdersQuery({
|
|
159
|
+
page: 1,
|
|
160
|
+
limit: 10
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (isLoading) return <div>Loading...</div>;
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div>
|
|
167
|
+
<h2>Order History</h2>
|
|
168
|
+
{oldOrders?.results?.map((order) => (
|
|
169
|
+
<div key={order.id}>
|
|
170
|
+
<span>#{order.number}</span>
|
|
171
|
+
<span>{new Date(order.created_date).toLocaleDateString()}</span>
|
|
172
|
+
</div>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// B2B Quotations (if available)
|
|
179
|
+
const QuotationsList = () => {
|
|
180
|
+
const { data: quotations, isLoading } = useGetQuotationsQuery({
|
|
181
|
+
page: 1,
|
|
182
|
+
limit: 10
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (isLoading) return <div>Loading quotations...</div>;
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div>
|
|
189
|
+
<h2>Active Quotations</h2>
|
|
190
|
+
{quotations?.results?.map((quotation) => (
|
|
191
|
+
<div key={quotation.id}>
|
|
192
|
+
<h3>{quotation.title}</h3>
|
|
193
|
+
<p>Status: {quotation.status}</p>
|
|
194
|
+
</div>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Loyalty points (if available)
|
|
201
|
+
const LoyaltyInfo = () => {
|
|
202
|
+
const { data: balance, isLoading: balanceLoading } =
|
|
203
|
+
useGetLoyaltyBalanceQuery();
|
|
204
|
+
const { data: transactions, isLoading: transactionsLoading } =
|
|
205
|
+
useGetLoyaltyTransactionsQuery();
|
|
206
|
+
|
|
207
|
+
if (balanceLoading || transactionsLoading) return <div>Loading...</div>;
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div>
|
|
211
|
+
<h2>Loyalty Points</h2>
|
|
212
|
+
<p>Current Balance: {balance?.balance || 0} points</p>
|
|
213
|
+
|
|
214
|
+
<h3>Recent Transactions</h3>
|
|
215
|
+
<div>
|
|
216
|
+
{transactions?.results?.map((transaction, index) => (
|
|
217
|
+
<div key={index} className="border-b py-2">
|
|
218
|
+
<span>{transaction.amount} points</span>
|
|
219
|
+
<span className="text-gray-500 ml-2">
|
|
220
|
+
{new Date(transaction.created_date).toLocaleDateString()}
|
|
221
|
+
</span>
|
|
222
|
+
</div>
|
|
223
|
+
))}
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Basket offers
|
|
230
|
+
const BasketOffers = () => {
|
|
231
|
+
const { data: currentOffers } = useGetBasketOffersQuery();
|
|
232
|
+
const { data: futureOffers } = useGetFutureBasketOffersQuery();
|
|
233
|
+
const { data: expiredOffers } = useGetExpiredBasketOffersQuery();
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div>
|
|
237
|
+
<h2>Basket Offers</h2>
|
|
238
|
+
|
|
239
|
+
{currentOffers && (
|
|
240
|
+
<div>
|
|
241
|
+
<h3>Active Offers</h3>
|
|
242
|
+
{/* Offer list */}
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{futureOffers && (
|
|
247
|
+
<div>
|
|
248
|
+
<h3>Future Offers</h3>
|
|
249
|
+
{/* Future offers */}
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
};
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Mutation Hooks - Correct Usage Patterns
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
'use client';
|
|
261
|
+
|
|
262
|
+
const ProfileForm = () => {
|
|
263
|
+
const [updateProfile, { isLoading }] = useUpdateProfileMutation();
|
|
264
|
+
const [updateEmail, { isLoading: emailLoading }] = useUpdateEmailMutation();
|
|
265
|
+
const [updatePassword, { isLoading: passwordLoading }] =
|
|
266
|
+
useUpdatePasswordMutation();
|
|
267
|
+
|
|
268
|
+
const handleUpdateProfile = async (formData) => {
|
|
269
|
+
try {
|
|
270
|
+
const result = await updateProfile({
|
|
271
|
+
first_name: formData.firstName,
|
|
272
|
+
last_name: formData.lastName,
|
|
273
|
+
phone: formData.phone,
|
|
274
|
+
date_of_birth: formData.birthDate,
|
|
275
|
+
gender: formData.gender,
|
|
276
|
+
sms_allowed: formData.smsAllowed,
|
|
277
|
+
email_allowed: formData.emailAllowed
|
|
278
|
+
}).unwrap();
|
|
279
|
+
|
|
280
|
+
// Success handling
|
|
281
|
+
console.log('Profile updated:', result);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
// Error handling
|
|
284
|
+
console.error('Profile update error:', error);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const handleChangeEmail = async (emailData) => {
|
|
289
|
+
try {
|
|
290
|
+
await updateEmail({
|
|
291
|
+
new_email: emailData.newEmail,
|
|
292
|
+
current_password: emailData.currentPassword
|
|
293
|
+
}).unwrap();
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error('Email change error:', error);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const handleChangePassword = async (passwordData) => {
|
|
300
|
+
try {
|
|
301
|
+
await updatePassword({
|
|
302
|
+
old_password: passwordData.oldPassword,
|
|
303
|
+
new_password1: passwordData.newPassword,
|
|
304
|
+
new_password2: passwordData.confirmPassword
|
|
305
|
+
}).unwrap();
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error('Password change error:', error);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<div className="space-y-6">
|
|
313
|
+
{/* Profile form */}
|
|
314
|
+
<button
|
|
315
|
+
onClick={handleUpdateProfile}
|
|
316
|
+
disabled={isLoading}
|
|
317
|
+
className="bg-blue-500 text-white p-2 rounded disabled:opacity-50"
|
|
318
|
+
>
|
|
319
|
+
{isLoading ? 'Saving...' : 'Update Profile'}
|
|
320
|
+
</button>
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Order cancellation
|
|
326
|
+
const OrderActions = ({ orderId }) => {
|
|
327
|
+
const [cancelOrder, { isLoading }] = useCancelOrderMutation();
|
|
328
|
+
const { data: cancellationReasons } = useGetCancellationReasonsQuery();
|
|
329
|
+
|
|
330
|
+
const handleCancelOrder = async (reasonId) => {
|
|
331
|
+
try {
|
|
332
|
+
await cancelOrder({
|
|
333
|
+
id: orderId,
|
|
334
|
+
reason: reasonId
|
|
335
|
+
}).unwrap();
|
|
336
|
+
|
|
337
|
+
// Show success message
|
|
338
|
+
} catch (error) {
|
|
339
|
+
console.error('Order cancellation error:', error);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<div>
|
|
345
|
+
<h3>Cancel Order</h3>
|
|
346
|
+
{cancellationReasons?.map((reason) => (
|
|
347
|
+
<button
|
|
348
|
+
key={reason.id}
|
|
349
|
+
onClick={() => handleCancelOrder(reason.id)}
|
|
350
|
+
disabled={isLoading}
|
|
351
|
+
className="block w-full text-left p-2 hover:bg-gray-100 disabled:opacity-50"
|
|
352
|
+
>
|
|
353
|
+
{reason.text}
|
|
354
|
+
</button>
|
|
355
|
+
))}
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Contact form
|
|
361
|
+
const ContactForm = () => {
|
|
362
|
+
const [sendContact, { isLoading }] = useSendContactMutation();
|
|
363
|
+
const { data: subjects } = useGetContactSubjectsQuery();
|
|
364
|
+
|
|
365
|
+
const handleSubmit = async (formData) => {
|
|
366
|
+
try {
|
|
367
|
+
const form = new FormData();
|
|
368
|
+
form.append('subject', formData.subject);
|
|
369
|
+
form.append('message', formData.message);
|
|
370
|
+
form.append('order_number', formData.orderNumber || '');
|
|
371
|
+
|
|
372
|
+
if (formData.attachment) {
|
|
373
|
+
form.append('attachment', formData.attachment);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
await sendContact(form).unwrap();
|
|
377
|
+
|
|
378
|
+
// Success message
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error('Contact form submission error:', error);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<form onSubmit={handleSubmit}>
|
|
386
|
+
<select name="subject" required>
|
|
387
|
+
<option value="">Select subject</option>
|
|
388
|
+
{subjects?.map((subject) => (
|
|
389
|
+
<option key={subject.id} value={subject.id}>
|
|
390
|
+
{subject.text}
|
|
391
|
+
</option>
|
|
392
|
+
))}
|
|
393
|
+
</select>
|
|
394
|
+
|
|
395
|
+
<textarea name="message" required placeholder="Your message" />
|
|
396
|
+
|
|
397
|
+
<input type="file" name="attachment" />
|
|
398
|
+
|
|
399
|
+
<button
|
|
400
|
+
type="submit"
|
|
401
|
+
disabled={isLoading}
|
|
402
|
+
className="bg-blue-500 text-white p-2 rounded disabled:opacity-50"
|
|
403
|
+
>
|
|
404
|
+
{isLoading ? 'Sending...' : 'Send'}
|
|
405
|
+
</button>
|
|
406
|
+
</form>
|
|
407
|
+
);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// Account anonymization (GDPR)
|
|
411
|
+
const AccountSettings = () => {
|
|
412
|
+
const [anonymize, { isLoading }] = useAnonymizeMutation();
|
|
413
|
+
|
|
414
|
+
const handleAnonymize = async () => {
|
|
415
|
+
if (confirm('Are you sure you want to permanently delete your account?')) {
|
|
416
|
+
try {
|
|
417
|
+
const result = await anonymize().unwrap();
|
|
418
|
+
|
|
419
|
+
// Redirect user to logout page
|
|
420
|
+
window.location.href = '/logout';
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error('Account anonymization error:', error);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<div className="bg-red-50 p-4 rounded border border-red-200">
|
|
429
|
+
<h3 className="text-red-800 font-semibold">Delete Account</h3>
|
|
430
|
+
<p className="text-red-700 text-sm mb-4">
|
|
431
|
+
This action cannot be undone. All your data will be permanently deleted.
|
|
432
|
+
</p>
|
|
433
|
+
<button
|
|
434
|
+
onClick={handleAnonymize}
|
|
435
|
+
disabled={isLoading}
|
|
436
|
+
className="bg-red-500 text-white px-4 py-2 rounded disabled:opacity-50"
|
|
437
|
+
>
|
|
438
|
+
{isLoading ? 'Deleting...' : 'Delete Account'}
|
|
439
|
+
</button>
|
|
440
|
+
</div>
|
|
441
|
+
);
|
|
442
|
+
};
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## State Management
|
|
446
|
+
|
|
447
|
+
### Redux Store
|
|
448
|
+
|
|
449
|
+
```tsx
|
|
450
|
+
'use client';
|
|
451
|
+
|
|
452
|
+
import { useAppSelector, useAppDispatch } from '@akinon/next/redux/store';
|
|
453
|
+
|
|
454
|
+
const AccountComponent = () => {
|
|
455
|
+
const dispatch = useAppDispatch();
|
|
456
|
+
const { user, isLoading } = useAppSelector((state) => state.account);
|
|
457
|
+
|
|
458
|
+
// Redux actions
|
|
459
|
+
const handleUpdate = () => {
|
|
460
|
+
dispatch(/* account action */);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
return <div>{/* Component */}</div>;
|
|
464
|
+
};
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Form Handling
|
|
468
|
+
|
|
469
|
+
### React Hook Form
|
|
470
|
+
|
|
471
|
+
```tsx
|
|
472
|
+
'use client';
|
|
473
|
+
|
|
474
|
+
import { useForm } from 'react-hook-form';
|
|
475
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
476
|
+
import * as yup from 'yup';
|
|
477
|
+
import { useLocalization } from '@akinon/next/hooks/useLocalization';
|
|
478
|
+
|
|
479
|
+
const profileSchema = yup.object({
|
|
480
|
+
first_name: yup.string().required('First name is required'),
|
|
481
|
+
last_name: yup.string().required('Last name is required'),
|
|
482
|
+
phone: yup.string().required('Phone is required'),
|
|
483
|
+
date_of_birth: yup.date().nullable(),
|
|
484
|
+
gender: yup.string(),
|
|
485
|
+
sms_allowed: yup.boolean(),
|
|
486
|
+
email_allowed: yup.boolean()
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const ProfileForm = () => {
|
|
490
|
+
const { t } = useLocalization();
|
|
491
|
+
const [updateProfile, { isLoading }] = useUpdateProfileMutation();
|
|
492
|
+
|
|
493
|
+
const {
|
|
494
|
+
register,
|
|
495
|
+
handleSubmit,
|
|
496
|
+
formState: { errors }
|
|
497
|
+
} = useForm({
|
|
498
|
+
resolver: yupResolver(profileSchema)
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const onSubmit = async (data) => {
|
|
502
|
+
try {
|
|
503
|
+
await updateProfile(data).unwrap();
|
|
504
|
+
// Success message
|
|
505
|
+
} catch (error) {
|
|
506
|
+
// Error handling
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
return (
|
|
511
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
512
|
+
<input
|
|
513
|
+
{...register('first_name')}
|
|
514
|
+
placeholder="First Name"
|
|
515
|
+
className="border p-2 rounded"
|
|
516
|
+
/>
|
|
517
|
+
{errors.first_name && (
|
|
518
|
+
<span className="text-red-500">{errors.first_name.message}</span>
|
|
519
|
+
)}
|
|
520
|
+
|
|
521
|
+
<input
|
|
522
|
+
{...register('last_name')}
|
|
523
|
+
placeholder="Last Name"
|
|
524
|
+
className="border p-2 rounded"
|
|
525
|
+
/>
|
|
526
|
+
{errors.last_name && (
|
|
527
|
+
<span className="text-red-500">{errors.last_name.message}</span>
|
|
528
|
+
)}
|
|
529
|
+
|
|
530
|
+
<button
|
|
531
|
+
type="submit"
|
|
532
|
+
disabled={isLoading}
|
|
533
|
+
className="bg-blue-500 text-white p-2 rounded"
|
|
534
|
+
>
|
|
535
|
+
{isLoading ? 'Saving...' : 'Save'}
|
|
536
|
+
</button>
|
|
537
|
+
</form>
|
|
538
|
+
);
|
|
539
|
+
};
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Styling
|
|
543
|
+
|
|
544
|
+
### TailwindCSS
|
|
545
|
+
|
|
546
|
+
```tsx
|
|
547
|
+
const AccountPage = () => {
|
|
548
|
+
return (
|
|
549
|
+
<div className="container mx-auto px-4 py-8">
|
|
550
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
551
|
+
<div className="bg-white rounded-lg shadow p-6">
|
|
552
|
+
<h2 className="text-xl font-semibold mb-4">Profile</h2>
|
|
553
|
+
{/* Content */}
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
);
|
|
558
|
+
};
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### Conditional Classes
|
|
562
|
+
|
|
563
|
+
```tsx
|
|
564
|
+
import { clsx } from 'clsx';
|
|
565
|
+
|
|
566
|
+
const StatusBadge = ({ status }) => {
|
|
567
|
+
return (
|
|
568
|
+
<span
|
|
569
|
+
className={clsx('px-2 py-1 rounded text-sm', {
|
|
570
|
+
'bg-green-100 text-green-800': status === 'completed',
|
|
571
|
+
'bg-yellow-100 text-yellow-800': status === 'pending',
|
|
572
|
+
'bg-red-100 text-red-800': status === 'cancelled'
|
|
573
|
+
})}
|
|
574
|
+
>
|
|
575
|
+
{status}
|
|
576
|
+
</span>
|
|
577
|
+
);
|
|
578
|
+
};
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
## Localization
|
|
582
|
+
|
|
583
|
+
### Usage
|
|
584
|
+
|
|
585
|
+
```tsx
|
|
586
|
+
import { useLocalization } from '@akinon/next/hooks/useLocalization';
|
|
587
|
+
|
|
588
|
+
const AccountComponent = () => {
|
|
589
|
+
const { t } = useLocalization();
|
|
590
|
+
|
|
591
|
+
return (
|
|
592
|
+
<div>
|
|
593
|
+
<h1>{t('account.title')}</h1>
|
|
594
|
+
<p>{t('account.welcome_message', { name: 'User' })}</p>
|
|
595
|
+
</div>
|
|
596
|
+
);
|
|
597
|
+
};
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
## Error Handling
|
|
601
|
+
|
|
602
|
+
### API Errors
|
|
603
|
+
|
|
604
|
+
```tsx
|
|
605
|
+
const AccountComponent = () => {
|
|
606
|
+
const { data, error, isLoading } = useGetProfileInfoQuery();
|
|
607
|
+
|
|
608
|
+
if (error) {
|
|
609
|
+
return (
|
|
610
|
+
<div className="bg-red-50 border border-red-200 rounded p-4">
|
|
611
|
+
<p className="text-red-700">
|
|
612
|
+
{error.data?.message || 'An error occurred'}
|
|
613
|
+
</p>
|
|
614
|
+
</div>
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (isLoading) {
|
|
619
|
+
return <div className="animate-pulse">Loading...</div>;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return <div>{/* Normal content */}</div>;
|
|
623
|
+
};
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
## Performance
|
|
627
|
+
|
|
628
|
+
### Loading States
|
|
629
|
+
|
|
630
|
+
```tsx
|
|
631
|
+
const LoadingSkeleton = () => (
|
|
632
|
+
<div className="animate-pulse space-y-4">
|
|
633
|
+
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
|
634
|
+
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
|
635
|
+
<div className="h-32 bg-gray-200 rounded"></div>
|
|
636
|
+
</div>
|
|
637
|
+
);
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Lazy Loading
|
|
641
|
+
|
|
642
|
+
```tsx
|
|
643
|
+
import { LazyComponent } from '@akinon/next/components/lazy-component';
|
|
644
|
+
|
|
645
|
+
const LazyOrderHistory = LazyComponent(() => import('./OrderHistory'));
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
## Best Practices
|
|
649
|
+
|
|
650
|
+
### TypeScript
|
|
651
|
+
|
|
652
|
+
```tsx
|
|
653
|
+
interface User {
|
|
654
|
+
id: number;
|
|
655
|
+
first_name: string;
|
|
656
|
+
last_name: string;
|
|
657
|
+
email: string;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
interface Order {
|
|
661
|
+
id: number;
|
|
662
|
+
number: string;
|
|
663
|
+
status: string;
|
|
664
|
+
total: number;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
interface Address {
|
|
668
|
+
id: number;
|
|
669
|
+
title: string;
|
|
670
|
+
address: string;
|
|
671
|
+
city: string;
|
|
672
|
+
district: string;
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### File Organization
|
|
677
|
+
|
|
678
|
+
```
|
|
679
|
+
account/
|
|
680
|
+
├── page.tsx # Main account page
|
|
681
|
+
├── layout.tsx # Layout (optional)
|
|
682
|
+
├── profile/
|
|
683
|
+
│ └── page.tsx # Profile page
|
|
684
|
+
├── orders/
|
|
685
|
+
│ ├── page.tsx # Orders list
|
|
686
|
+
│ └── [id]/
|
|
687
|
+
│ └── page.tsx # Order detail
|
|
688
|
+
└── addresses/
|
|
689
|
+
├── page.tsx # Address list
|
|
690
|
+
└── new/
|
|
691
|
+
└── page.tsx # New address
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
**Note:** These instructions are based on Project Zero's existing API and hook structures. Follow existing patterns when adding new features.
|
|
697
|
+
|
|
698
|
+
## Important Notes
|
|
699
|
+
|
|
700
|
+
### Hook Usage Rules
|
|
701
|
+
|
|
702
|
+
1. **Query hooks** run automatically, no manual triggering required
|
|
703
|
+
2. **Mutation hooks** return array destructuring with `[mutationFn, { isLoading, error }]`
|
|
704
|
+
3. Use **unwrap()** for promise-based error handling
|
|
705
|
+
4. **isLoading** state should be used in UI for loading states
|
|
706
|
+
|
|
707
|
+
### Error Handling Best Practices
|
|
708
|
+
|
|
709
|
+
```tsx
|
|
710
|
+
// RTK Query error handling
|
|
711
|
+
const { data, error, isLoading } = useGetOrdersQuery();
|
|
712
|
+
|
|
713
|
+
if (error) {
|
|
714
|
+
// RTK Query error structure
|
|
715
|
+
const errorMessage =
|
|
716
|
+
error.data?.message || error.error || 'An error occurred';
|
|
717
|
+
return <div className="text-red-500">{errorMessage}</div>;
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### Conditional Rendering
|
|
722
|
+
|
|
723
|
+
```tsx
|
|
724
|
+
// Show alternative content when no data
|
|
725
|
+
const OrdersList = () => {
|
|
726
|
+
const { data: orders, isLoading } = useGetOrdersQuery();
|
|
727
|
+
|
|
728
|
+
if (isLoading) return <LoadingSkeleton />;
|
|
729
|
+
|
|
730
|
+
if (!orders?.results?.length) {
|
|
731
|
+
return (
|
|
732
|
+
<div className="text-center py-8">
|
|
733
|
+
<p>You don't have any orders yet.</p>
|
|
734
|
+
<Link href="/products" className="text-blue-500">
|
|
735
|
+
Start shopping
|
|
736
|
+
</Link>
|
|
737
|
+
</div>
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return (
|
|
742
|
+
<div>
|
|
743
|
+
{orders.results.map((order) => (
|
|
744
|
+
<OrderCard key={order.id} order={order} />
|
|
745
|
+
))}
|
|
746
|
+
</div>
|
|
747
|
+
);
|
|
748
|
+
};
|
|
749
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: '**'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Edge Cases & Troubleshooting Instructions
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
This document contains common edge cases, bugs, and troubleshooting steps encountered in Project Zero Next.js development. These are real-world issues that have caused development delays and should be checked first when similar problems occur.
|
|
10
|
+
|
|
11
|
+
## Checkout Flow Edge Cases
|
|
12
|
+
|
|
13
|
+
### 1. Order Selection Page Request Timing
|
|
14
|
+
|
|
15
|
+
**Problem:** `orderselectionpage` request should NOT be called after `walletselectionpage` or `walletpaymentpage` requests.
|
|
16
|
+
|
|
17
|
+
This causes checkout flow to break after wallet payment steps, order selection shows incorrect state, and payment flow gets reset unexpectedly. After `walletselectionpage` or `walletpaymentpage` requests are made, `orderselectionpage` request should not be called. Always check the sequence of checkout API calls.
|
|
18
|
+
|
|
19
|
+
### 2. Guest Login with URL Parameters
|
|
20
|
+
|
|
21
|
+
**Problem:** Adding `?page=IndexPage` to checkout URLs breaks guest login functionality.
|
|
22
|
+
|
|
23
|
+
Guest login fails on checkout URLs with `?page=IndexPage` but works fine without the parameter. This parameter interferes with authentication middleware routing. Avoid adding unnecessary URL parameters to checkout flows.
|
|
24
|
+
|
|
25
|
+
### 3. Agreement Error After 3D Payment Redirect
|
|
26
|
+
|
|
27
|
+
**Problem:** "You must accept agreement to continue" error appears after 3D payment or redirections, but the issue is NOT related to agreements.
|
|
28
|
+
|
|
29
|
+
Agreement error occurs after successful 3D payment and checkout flow breaks after external redirections. The error message is misleading - actually `preOrder` data gets reset/lost during payment redirections. Don't assume agreement errors are actually about agreements.
|
|
30
|
+
|
|
31
|
+
### 4. Address Visibility with fetchCheckout
|
|
32
|
+
|
|
33
|
+
**Problem:** When `fetchCheckout` request includes `page=IndexPage` parameter, addresses don't show in checkout and the entire flow breaks.
|
|
34
|
+
|
|
35
|
+
Addresses are missing from checkout page, shipping options don't load, and checkout flow is completely broken. Avoid adding unnecessary parameters to core checkout requests and validate that all checkout data loads properly.
|
|
36
|
+
|
|
37
|
+
## Plugin & Configuration Edge Cases
|
|
38
|
+
|
|
39
|
+
### 5. Attribute-Based Shipping Options Typo
|
|
40
|
+
|
|
41
|
+
**Problem:** `attribute_based_shipping_options` was incorrectly entered as `atribute_based` (missing 't') in brand configurations.
|
|
42
|
+
|
|
43
|
+
Shipping options don't work for specific brands and debugging is difficult because the setting exists but is misspelled. This can cause days of debugging. Always double-check configuration property names.
|
|
44
|
+
|
|
45
|
+
### 6. Apple Pay Plugin Configuration
|
|
46
|
+
|
|
47
|
+
**Problem:** Apple Pay can work as both `confirmation` and `wallet` payment types, but for the plugin to work properly, it should NOT be configured as `confirmation` type.
|
|
48
|
+
|
|
49
|
+
Apple Pay flow breaks in certain scenarios, payment confirmation issues occur, and plugin conflicts with checkout flow. Always configure Apple Pay as wallet type.
|
|
50
|
+
|
|
51
|
+
## Build & Development Edge Cases
|
|
52
|
+
|
|
53
|
+
### 7. Build Failures Due to yarn.lock Corruption
|
|
54
|
+
|
|
55
|
+
**Problem:** Build fails with apparent TypeScript errors in `@akinon/next` components, but the real issue is a corrupted `yarn.lock` file.
|
|
56
|
+
|
|
57
|
+
Build fails with nonsensical type errors, components from `@akinon/next` are flagged with errors, and multiple versions of the same package exist in lock file.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Check for duplicate packages in yarn.lock
|
|
61
|
+
yarn list --pattern "@akinon/next" --depth=0
|
|
62
|
+
|
|
63
|
+
# If multiple versions exist, clean up
|
|
64
|
+
rm yarn.lock
|
|
65
|
+
rm -rf node_modules
|
|
66
|
+
yarn install
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Regularly check `yarn.lock` for duplicate dependencies and use `yarn dedupe` after adding new packages.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
**Remember:** Edge cases often have misleading error messages. Always investigate the root cause rather than fixing symptoms. The most time-consuming bugs are usually simple configuration or timing issues.
|
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# projectzeronext
|
|
2
2
|
|
|
3
|
+
## 1.99.0-rc.68
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3d3fe05: ZERO-3639: Add edge cases instruction file
|
|
8
|
+
- fdd9974: ZERO-3636: Add account instruction file
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- @akinon/next@1.99.0-rc.68
|
|
13
|
+
- @akinon/pz-akifast@1.99.0-rc.68
|
|
14
|
+
- @akinon/pz-b2b@1.99.0-rc.68
|
|
15
|
+
- @akinon/pz-basket-gift-pack@1.99.0-rc.68
|
|
16
|
+
- @akinon/pz-bkm@1.99.0-rc.68
|
|
17
|
+
- @akinon/pz-checkout-gift-pack@1.99.0-rc.68
|
|
18
|
+
- @akinon/pz-click-collect@1.99.0-rc.68
|
|
19
|
+
- @akinon/pz-credit-payment@1.99.0-rc.68
|
|
20
|
+
- @akinon/pz-gpay@1.99.0-rc.68
|
|
21
|
+
- @akinon/pz-hepsipay@1.99.0-rc.68
|
|
22
|
+
- @akinon/pz-masterpass@1.99.0-rc.68
|
|
23
|
+
- @akinon/pz-one-click-checkout@1.99.0-rc.68
|
|
24
|
+
- @akinon/pz-otp@1.99.0-rc.68
|
|
25
|
+
- @akinon/pz-pay-on-delivery@1.99.0-rc.68
|
|
26
|
+
- @akinon/pz-saved-card@1.99.0-rc.68
|
|
27
|
+
- @akinon/pz-similar-products@1.99.0-rc.68
|
|
28
|
+
- @akinon/pz-tabby-extension@1.99.0-rc.68
|
|
29
|
+
- @akinon/pz-tamara-extension@1.99.0-rc.68
|
|
30
|
+
|
|
31
|
+
## 1.99.0-rc.67
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- @akinon/next@1.99.0-rc.67
|
|
36
|
+
- @akinon/pz-akifast@1.99.0-rc.67
|
|
37
|
+
- @akinon/pz-b2b@1.99.0-rc.67
|
|
38
|
+
- @akinon/pz-basket-gift-pack@1.99.0-rc.67
|
|
39
|
+
- @akinon/pz-bkm@1.99.0-rc.67
|
|
40
|
+
- @akinon/pz-checkout-gift-pack@1.99.0-rc.67
|
|
41
|
+
- @akinon/pz-click-collect@1.99.0-rc.67
|
|
42
|
+
- @akinon/pz-credit-payment@1.99.0-rc.67
|
|
43
|
+
- @akinon/pz-gpay@1.99.0-rc.67
|
|
44
|
+
- @akinon/pz-hepsipay@1.99.0-rc.67
|
|
45
|
+
- @akinon/pz-masterpass@1.99.0-rc.67
|
|
46
|
+
- @akinon/pz-one-click-checkout@1.99.0-rc.67
|
|
47
|
+
- @akinon/pz-otp@1.99.0-rc.67
|
|
48
|
+
- @akinon/pz-pay-on-delivery@1.99.0-rc.67
|
|
49
|
+
- @akinon/pz-saved-card@1.99.0-rc.67
|
|
50
|
+
- @akinon/pz-similar-products@1.99.0-rc.67
|
|
51
|
+
- @akinon/pz-tabby-extension@1.99.0-rc.67
|
|
52
|
+
- @akinon/pz-tamara-extension@1.99.0-rc.67
|
|
53
|
+
|
|
3
54
|
## 1.99.0-rc.66
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectzeronext",
|
|
3
|
-
"version": "1.99.0-rc.
|
|
3
|
+
"version": "1.99.0-rc.68",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"scripts": {
|
|
@@ -24,22 +24,24 @@
|
|
|
24
24
|
"test:middleware": "jest middleware-matcher.test.ts --bail"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@akinon/next": "1.99.0-rc.
|
|
28
|
-
"@akinon/pz-akifast": "1.99.0-rc.
|
|
29
|
-
"@akinon/pz-b2b": "1.99.0-rc.
|
|
30
|
-
"@akinon/pz-basket-gift-pack": "1.99.0-rc.
|
|
31
|
-
"@akinon/pz-bkm": "1.99.0-rc.
|
|
32
|
-
"@akinon/pz-checkout-gift-pack": "1.99.0-rc.
|
|
33
|
-
"@akinon/pz-click-collect": "1.99.0-rc.
|
|
34
|
-
"@akinon/pz-credit-payment": "1.99.0-rc.
|
|
35
|
-
"@akinon/pz-gpay": "1.99.0-rc.
|
|
36
|
-
"@akinon/pz-
|
|
37
|
-
"@akinon/pz-
|
|
38
|
-
"@akinon/pz-
|
|
39
|
-
"@akinon/pz-
|
|
40
|
-
"@akinon/pz-
|
|
41
|
-
"@akinon/pz-
|
|
42
|
-
"@akinon/pz-
|
|
27
|
+
"@akinon/next": "1.99.0-rc.68",
|
|
28
|
+
"@akinon/pz-akifast": "1.99.0-rc.68",
|
|
29
|
+
"@akinon/pz-b2b": "1.99.0-rc.68",
|
|
30
|
+
"@akinon/pz-basket-gift-pack": "1.99.0-rc.68",
|
|
31
|
+
"@akinon/pz-bkm": "1.99.0-rc.68",
|
|
32
|
+
"@akinon/pz-checkout-gift-pack": "1.99.0-rc.68",
|
|
33
|
+
"@akinon/pz-click-collect": "1.99.0-rc.68",
|
|
34
|
+
"@akinon/pz-credit-payment": "1.99.0-rc.68",
|
|
35
|
+
"@akinon/pz-gpay": "1.99.0-rc.68",
|
|
36
|
+
"@akinon/pz-hepsipay": "1.99.0-rc.68",
|
|
37
|
+
"@akinon/pz-masterpass": "1.99.0-rc.68",
|
|
38
|
+
"@akinon/pz-one-click-checkout": "1.99.0-rc.68",
|
|
39
|
+
"@akinon/pz-otp": "1.99.0-rc.68",
|
|
40
|
+
"@akinon/pz-pay-on-delivery": "1.99.0-rc.68",
|
|
41
|
+
"@akinon/pz-saved-card": "1.99.0-rc.68",
|
|
42
|
+
"@akinon/pz-similar-products": "1.99.0-rc.68",
|
|
43
|
+
"@akinon/pz-tabby-extension": "1.99.0-rc.68",
|
|
44
|
+
"@akinon/pz-tamara-extension": "1.99.0-rc.68",
|
|
43
45
|
"@hookform/resolvers": "2.9.0",
|
|
44
46
|
"@next/third-parties": "14.1.0",
|
|
45
47
|
"@react-google-maps/api": "2.17.1",
|
|
@@ -62,7 +64,7 @@
|
|
|
62
64
|
"yup": "0.32.11"
|
|
63
65
|
},
|
|
64
66
|
"devDependencies": {
|
|
65
|
-
"@akinon/eslint-plugin-projectzero": "1.99.0-rc.
|
|
67
|
+
"@akinon/eslint-plugin-projectzero": "1.99.0-rc.68",
|
|
66
68
|
"@semantic-release/changelog": "6.0.2",
|
|
67
69
|
"@semantic-release/exec": "6.0.3",
|
|
68
70
|
"@semantic-release/git": "10.0.1",
|