create-brainerce-store 1.14.2 → 1.14.4
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/dist/index.js +47 -4
- package/messages/en.json +36 -4
- package/messages/he.json +36 -4
- package/package.json +1 -1
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +81 -74
- package/templates/nextjs/base/src/app/account/layout.tsx +9 -0
- package/templates/nextjs/base/src/app/account/page.tsx +122 -112
- package/templates/nextjs/base/src/app/cart/layout.tsx +9 -0
- package/templates/nextjs/base/src/app/checkout/layout.tsx +9 -0
- package/templates/nextjs/base/src/app/checkout/page.tsx +107 -8
- package/templates/nextjs/base/src/app/layout.tsx.ejs +29 -1
- package/templates/nextjs/base/src/app/login/layout.tsx +9 -0
- package/templates/nextjs/base/src/app/order-confirmation/layout.tsx +9 -0
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +5 -1
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +1 -6
- package/templates/nextjs/base/src/app/products/layout.tsx +18 -0
- package/templates/nextjs/base/src/app/products/page.tsx +1 -0
- package/templates/nextjs/base/src/app/register/layout.tsx +9 -0
- package/templates/nextjs/base/src/app/register/page.tsx +1 -0
- package/templates/nextjs/base/src/app/verify-email/page.tsx +1 -1
- package/templates/nextjs/base/src/components/account/address-book.tsx +432 -0
- package/templates/nextjs/base/src/components/account/order-history.tsx +2 -1
- package/templates/nextjs/base/src/components/auth/register-form.tsx +232 -184
- package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -153
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +359 -305
- package/templates/nextjs/base/src/components/products/product-card.tsx +159 -43
- package/templates/nextjs/base/src/components/products/stock-badge.tsx +60 -53
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +40 -7
- package/templates/nextjs/base/src/lib/auth.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.14.
|
|
34
|
+
version: "1.14.4",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
|
@@ -412,12 +412,22 @@ async function fetchStoreInfo(connectionId, baseUrl = "https://api.brainerce.com
|
|
|
412
412
|
// src/utils/logger.ts
|
|
413
413
|
var import_chalk = __toESM(require("chalk"));
|
|
414
414
|
var logger = {
|
|
415
|
-
banner() {
|
|
415
|
+
banner(version) {
|
|
416
416
|
console.log();
|
|
417
|
-
console.log(
|
|
417
|
+
console.log(
|
|
418
|
+
import_chalk.default.bold.cyan(" create-brainerce-store") + (version ? import_chalk.default.dim(` v${version}`) : "")
|
|
419
|
+
);
|
|
418
420
|
console.log(import_chalk.default.dim(" Scaffold a production-ready e-commerce storefront"));
|
|
419
421
|
console.log();
|
|
420
422
|
},
|
|
423
|
+
updateAvailable(latest, current) {
|
|
424
|
+
console.log();
|
|
425
|
+
console.log(
|
|
426
|
+
import_chalk.default.yellow(` Update available: `) + import_chalk.default.dim(current) + import_chalk.default.yellow(" \u2192 ") + import_chalk.default.green.bold(latest)
|
|
427
|
+
);
|
|
428
|
+
console.log(import_chalk.default.dim(" Run: ") + import_chalk.default.white("npm install -g create-brainerce-store"));
|
|
429
|
+
console.log();
|
|
430
|
+
},
|
|
421
431
|
info(message) {
|
|
422
432
|
console.log(import_chalk.default.cyan(message));
|
|
423
433
|
},
|
|
@@ -446,10 +456,41 @@ function createSpinner(text) {
|
|
|
446
456
|
|
|
447
457
|
// src/index.ts
|
|
448
458
|
var pkg = require_package();
|
|
459
|
+
async function checkForUpdate(name, current) {
|
|
460
|
+
try {
|
|
461
|
+
const https2 = require("https");
|
|
462
|
+
return await new Promise((resolve) => {
|
|
463
|
+
const req = https2.get(
|
|
464
|
+
`https://registry.npmjs.org/${name}/latest`,
|
|
465
|
+
{ timeout: 3e3 },
|
|
466
|
+
(res) => {
|
|
467
|
+
let data = "";
|
|
468
|
+
res.on("data", (chunk) => data += chunk);
|
|
469
|
+
res.on("end", () => {
|
|
470
|
+
try {
|
|
471
|
+
const latest = JSON.parse(data).version;
|
|
472
|
+
resolve(latest && latest !== current ? latest : null);
|
|
473
|
+
} catch {
|
|
474
|
+
resolve(null);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
req.on("error", () => resolve(null));
|
|
480
|
+
req.on("timeout", () => {
|
|
481
|
+
req.destroy();
|
|
482
|
+
resolve(null);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
} catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
449
489
|
var program = new import_commander.Command();
|
|
450
490
|
program.name("create-brainerce-store").description("Scaffold a production-ready e-commerce storefront connected to Brainerce").version(pkg.version).argument("[project-name]", "Name for the project directory").option("--connection-id <id>", "Brainerce vibe-coded connection ID (vc_*)").option("--language <lang>", "Store language (en, he)").option("--framework <framework>", "Framework to use", "nextjs").option("--theme <theme>", "Theme to apply", "minimal").option("--pkg-manager <manager>", "Package manager (npm, pnpm, yarn, bun)").option("--no-git", "Skip git initialization").option("--no-install", "Skip dependency installation").action(async (projectNameArg, options) => {
|
|
451
491
|
try {
|
|
452
|
-
logger.banner();
|
|
492
|
+
logger.banner(pkg.version);
|
|
493
|
+
const updateCheck = checkForUpdate(pkg.name, pkg.version);
|
|
453
494
|
let projectName = projectNameArg;
|
|
454
495
|
let connectionId = options.connectionId;
|
|
455
496
|
let language = options.language;
|
|
@@ -547,6 +588,8 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
|
|
|
547
588
|
logger.step(`${pkgManager}${pkgManager === "npm" ? " run" : ""} dev`);
|
|
548
589
|
logger.info(`
|
|
549
590
|
Your store will be running at http://localhost:3000`);
|
|
591
|
+
const latestVersion = await updateCheck;
|
|
592
|
+
if (latestVersion) logger.updateAvailable(latestVersion, pkg.version);
|
|
550
593
|
} catch (err) {
|
|
551
594
|
if (err instanceof Error && err.message === "PROMPT_CANCELLED") {
|
|
552
595
|
logger.info("\nSetup cancelled.");
|
package/messages/en.json
CHANGED
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"sortNameAZ": "Name A-Z",
|
|
63
63
|
"sortNameZA": "Name Z-A",
|
|
64
64
|
"sortPriceLow": "Price: Low to High",
|
|
65
|
-
"sortPriceHigh": "Price: High to Low"
|
|
65
|
+
"sortPriceHigh": "Price: High to Low",
|
|
66
|
+
"selectOptions": "Select Options"
|
|
66
67
|
},
|
|
67
68
|
"productDetail": {
|
|
68
69
|
"notFound": "Product not found.",
|
|
@@ -173,7 +174,11 @@
|
|
|
173
174
|
"addressRequired": "Address is required",
|
|
174
175
|
"cityRequired": "City is required",
|
|
175
176
|
"postalCodeRequired": "Postal code is required",
|
|
176
|
-
"countryRequired": "Country is required"
|
|
177
|
+
"countryRequired": "Country is required",
|
|
178
|
+
"privacyAcceptPrefix": "I have read and agree to the",
|
|
179
|
+
"privacyPolicyLink": "Privacy Policy",
|
|
180
|
+
"privacyRequired": "You must accept the privacy policy to continue",
|
|
181
|
+
"acceptsMarketing": "Send me news, promotions, and updates by email"
|
|
177
182
|
},
|
|
178
183
|
"auth": {
|
|
179
184
|
"loginPageTitle": "Sign In",
|
|
@@ -239,7 +244,11 @@
|
|
|
239
244
|
"passwordResetSuccess": "Password reset successfully! Redirecting to login...",
|
|
240
245
|
"passwordsMustMatch": "Passwords must match",
|
|
241
246
|
"invalidResetLink": "Invalid reset link",
|
|
242
|
-
"invalidResetLinkDesc": "This password reset link is invalid or has expired. Please request a new one."
|
|
247
|
+
"invalidResetLinkDesc": "This password reset link is invalid or has expired. Please request a new one.",
|
|
248
|
+
"privacyAcceptPrefix": "I have read and agree to the",
|
|
249
|
+
"privacyPolicyLink": "Privacy Policy",
|
|
250
|
+
"privacyRequired": "You must accept the privacy policy to continue",
|
|
251
|
+
"acceptsMarketing": "Send me news, promotions, and updates by email"
|
|
243
252
|
},
|
|
244
253
|
"account": {
|
|
245
254
|
"pageTitle": "My Account",
|
|
@@ -272,7 +281,30 @@
|
|
|
272
281
|
"downloadsRemaining": "{used} of {limit} downloads used",
|
|
273
282
|
"unlimitedDownloads": "Unlimited downloads",
|
|
274
283
|
"expiresAt": "Expires {date}",
|
|
275
|
-
"noExpiry": "No expiry"
|
|
284
|
+
"noExpiry": "No expiry",
|
|
285
|
+
"addressBook": "Address Book",
|
|
286
|
+
"addAddress": "Add Address",
|
|
287
|
+
"editAddress": "Edit Address",
|
|
288
|
+
"deleteAddress": "Delete",
|
|
289
|
+
"setDefault": "Set as default",
|
|
290
|
+
"defaultAddress": "Default",
|
|
291
|
+
"noAddresses": "No saved addresses yet.",
|
|
292
|
+
"addressSaved": "Address saved",
|
|
293
|
+
"addressDeleted": "Address deleted",
|
|
294
|
+
"addressLabel": "Label (e.g. Home, Work)",
|
|
295
|
+
"line1": "Street Address",
|
|
296
|
+
"line2": "Apt, Suite, etc.",
|
|
297
|
+
"city": "City",
|
|
298
|
+
"region": "State / Region",
|
|
299
|
+
"postalCode": "Postal Code",
|
|
300
|
+
"country": "Country",
|
|
301
|
+
"isDefault": "Set as my default address"
|
|
302
|
+
},
|
|
303
|
+
"checkoutAddress": {
|
|
304
|
+
"saveToProfile": "Save this address to my profile?",
|
|
305
|
+
"saveYes": "Save",
|
|
306
|
+
"saveNo": "No thanks",
|
|
307
|
+
"addressSaved": "Address saved to your profile"
|
|
276
308
|
},
|
|
277
309
|
"orderConfirmation": {
|
|
278
310
|
"pageTitle": "Order Confirmation",
|
package/messages/he.json
CHANGED
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"sortNameAZ": "שם א-ת",
|
|
63
63
|
"sortNameZA": "שם ת-א",
|
|
64
64
|
"sortPriceLow": "מחיר: מהנמוך לגבוה",
|
|
65
|
-
"sortPriceHigh": "מחיר: מהגבוה לנמוך"
|
|
65
|
+
"sortPriceHigh": "מחיר: מהגבוה לנמוך",
|
|
66
|
+
"selectOptions": "בחר אפשרויות"
|
|
66
67
|
},
|
|
67
68
|
"productDetail": {
|
|
68
69
|
"notFound": "המוצר לא נמצא.",
|
|
@@ -173,7 +174,11 @@
|
|
|
173
174
|
"addressRequired": "כתובת היא שדה חובה",
|
|
174
175
|
"cityRequired": "עיר היא שדה חובה",
|
|
175
176
|
"postalCodeRequired": "מיקוד הוא שדה חובה",
|
|
176
|
-
"countryRequired": "מדינה היא שדה חובה"
|
|
177
|
+
"countryRequired": "מדינה היא שדה חובה",
|
|
178
|
+
"privacyAcceptPrefix": "קראתי ואני מסכים/ה ל",
|
|
179
|
+
"privacyPolicyLink": "מדיניות הפרטיות",
|
|
180
|
+
"privacyRequired": "יש לאשר את מדיניות הפרטיות כדי להמשיך",
|
|
181
|
+
"acceptsMarketing": "שלחו לי חדשות, מבצעים ועדכונים במייל"
|
|
177
182
|
},
|
|
178
183
|
"auth": {
|
|
179
184
|
"loginPageTitle": "התחברות",
|
|
@@ -239,7 +244,11 @@
|
|
|
239
244
|
"passwordResetSuccess": "...הסיסמא אופסה בהצלחה! מעביר להתחברות",
|
|
240
245
|
"passwordsMustMatch": "הסיסמאות חייבות להיות זהות",
|
|
241
246
|
"invalidResetLink": "קישור איפוס לא תקין",
|
|
242
|
-
"invalidResetLinkDesc": "קישור האיפוס הזה אינו תקין או שפג תוקפו. בקשו קישור חדש."
|
|
247
|
+
"invalidResetLinkDesc": "קישור האיפוס הזה אינו תקין או שפג תוקפו. בקשו קישור חדש.",
|
|
248
|
+
"privacyAcceptPrefix": "קראתי ואני מסכים/ה ל",
|
|
249
|
+
"privacyPolicyLink": "מדיניות הפרטיות",
|
|
250
|
+
"privacyRequired": "יש לאשר את מדיניות הפרטיות כדי להמשיך",
|
|
251
|
+
"acceptsMarketing": "שלחו לי חדשות, מבצעים ועדכונים במייל"
|
|
243
252
|
},
|
|
244
253
|
"account": {
|
|
245
254
|
"pageTitle": "החשבון שלי",
|
|
@@ -272,7 +281,30 @@
|
|
|
272
281
|
"downloadsRemaining": "{used} מתוך {limit} הורדות נוצלו",
|
|
273
282
|
"unlimitedDownloads": "הורדות ללא הגבלה",
|
|
274
283
|
"expiresAt": "פג תוקף {date}",
|
|
275
|
-
"noExpiry": "ללא תפוגה"
|
|
284
|
+
"noExpiry": "ללא תפוגה",
|
|
285
|
+
"addressBook": "ספר כתובות",
|
|
286
|
+
"addAddress": "הוסף כתובת",
|
|
287
|
+
"editAddress": "עריכת כתובת",
|
|
288
|
+
"deleteAddress": "מחיקה",
|
|
289
|
+
"setDefault": "הגדר כברירת מחדל",
|
|
290
|
+
"defaultAddress": "ברירת מחדל",
|
|
291
|
+
"noAddresses": "אין כתובות שמורות עדיין.",
|
|
292
|
+
"addressSaved": "הכתובת נשמרה",
|
|
293
|
+
"addressDeleted": "הכתובת נמחקה",
|
|
294
|
+
"addressLabel": "תווית (למשל בית, עבודה)",
|
|
295
|
+
"line1": "כתובת",
|
|
296
|
+
"line2": "דירה, קומה וכו'",
|
|
297
|
+
"city": "עיר",
|
|
298
|
+
"region": "מדינה / אזור",
|
|
299
|
+
"postalCode": "מיקוד",
|
|
300
|
+
"country": "מדינה",
|
|
301
|
+
"isDefault": "הגדר ככתובת ברירת המחדל שלי"
|
|
302
|
+
},
|
|
303
|
+
"checkoutAddress": {
|
|
304
|
+
"saveToProfile": "לשמור כתובת זו בפרופיל שלי?",
|
|
305
|
+
"saveYes": "שמור",
|
|
306
|
+
"saveNo": "לא תודה",
|
|
307
|
+
"addressSaved": "הכתובת נשמרה בפרופיל שלך"
|
|
276
308
|
},
|
|
277
309
|
"orderConfirmation": {
|
|
278
310
|
"pageTitle": "אישור הזמנה",
|
package/package.json
CHANGED
|
@@ -1,74 +1,81 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (currency)
|
|
74
|
-
|
|
1
|
+
/* global process, console, fetch */
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
/**
|
|
4
|
+
* Setup script: fetches store info from Brainerce using the connection ID
|
|
5
|
+
* and saves NEXT_PUBLIC_STORE_NAME (and other public fields) to .env.local.
|
|
6
|
+
*
|
|
7
|
+
* Run: node scripts/fetch-store-info.mjs
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
|
|
13
|
+
const envPath = join(process.cwd(), '.env.local');
|
|
14
|
+
|
|
15
|
+
if (!existsSync(envPath)) {
|
|
16
|
+
console.error(
|
|
17
|
+
'❌ .env.local not found. Create it first with NEXT_PUBLIC_BRAINERCE_CONNECTION_ID set.'
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const envContent = readFileSync(envPath, 'utf-8');
|
|
23
|
+
|
|
24
|
+
function getVar(content, key) {
|
|
25
|
+
const match = content.match(new RegExp(`^${key}=(.*)$`, 'm'));
|
|
26
|
+
return match ? match[1].trim() : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setVar(content, key, value) {
|
|
30
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
31
|
+
if (regex.test(content)) {
|
|
32
|
+
return content.replace(regex, `${key}=${value}`);
|
|
33
|
+
}
|
|
34
|
+
return content.trimEnd() + `\n${key}=${value}\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const connectionId = getVar(envContent, 'NEXT_PUBLIC_BRAINERCE_CONNECTION_ID');
|
|
38
|
+
const apiUrl = (getVar(envContent, 'BRAINERCE_API_URL') || 'https://api.brainerce.com').replace(
|
|
39
|
+
/\/$/,
|
|
40
|
+
''
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (!connectionId) {
|
|
44
|
+
console.error('❌ NEXT_PUBLIC_BRAINERCE_CONNECTION_ID is not set in .env.local');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`Fetching store info for connection: ${connectionId} ...`);
|
|
49
|
+
|
|
50
|
+
let storeInfo;
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`${apiUrl}/api/vc/${connectionId}/info`);
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
console.error(`❌ API returned ${res.status}: ${await res.text()}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
storeInfo = await res.json();
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error(`❌ Failed to reach ${apiUrl}: ${err.message}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const name = storeInfo.name;
|
|
64
|
+
const currency = storeInfo.currency;
|
|
65
|
+
|
|
66
|
+
if (!name) {
|
|
67
|
+
console.error('❌ Store info response has no `name` field:', storeInfo);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let updated = envContent;
|
|
72
|
+
updated = setVar(updated, 'NEXT_PUBLIC_STORE_NAME', name);
|
|
73
|
+
if (currency) {
|
|
74
|
+
updated = setVar(updated, 'NEXT_PUBLIC_STORE_CURRENCY', currency);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
writeFileSync(envPath, updated, 'utf-8');
|
|
78
|
+
|
|
79
|
+
console.log(`✓ NEXT_PUBLIC_STORE_NAME=${name}`);
|
|
80
|
+
if (currency) console.log(`✓ NEXT_PUBLIC_STORE_CURRENCY=${currency}`);
|
|
81
|
+
console.log('Done. Restart the dev server for changes to take effect.');
|
|
@@ -1,112 +1,122 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { useRouter } from 'next/navigation';
|
|
5
|
-
import type { CustomerProfile, Order } from 'brainerce';
|
|
6
|
-
import { getClient } from '@/lib/brainerce';
|
|
7
|
-
import { useAuth } from '@/providers/store-provider';
|
|
8
|
-
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
9
|
-
import { ProfileSection } from '@/components/account/profile-section';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const [
|
|
20
|
-
const [
|
|
21
|
-
const [
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
client.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import type { CustomerProfile, Order } from 'brainerce';
|
|
6
|
+
import { getClient } from '@/lib/brainerce';
|
|
7
|
+
import { useAuth } from '@/providers/store-provider';
|
|
8
|
+
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
9
|
+
import { ProfileSection } from '@/components/account/profile-section';
|
|
10
|
+
import { AddressBook } from '@/components/account/address-book';
|
|
11
|
+
import { OrderHistory } from '@/components/account/order-history';
|
|
12
|
+
import { useTranslations } from '@/lib/translations';
|
|
13
|
+
|
|
14
|
+
export default function AccountPage() {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const { isLoggedIn, authLoading, logout } = useAuth();
|
|
17
|
+
const t = useTranslations('account');
|
|
18
|
+
const tc = useTranslations('common');
|
|
19
|
+
const [profile, setProfile] = useState<CustomerProfile | null>(null);
|
|
20
|
+
const [orders, setOrders] = useState<Order[]>([]);
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
const [error, setError] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (authLoading) return;
|
|
26
|
+
|
|
27
|
+
if (!isLoggedIn) {
|
|
28
|
+
router.push('/login');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function loadAccountData() {
|
|
33
|
+
try {
|
|
34
|
+
const client = getClient();
|
|
35
|
+
const [profileResult, ordersResult] = await Promise.allSettled([
|
|
36
|
+
client.getMyProfile(),
|
|
37
|
+
client.getMyOrders({ limit: 20 }),
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
if (profileResult.status === 'fulfilled') {
|
|
41
|
+
setProfile(profileResult.value);
|
|
42
|
+
} else {
|
|
43
|
+
setError('Failed to load profile.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (ordersResult.status === 'fulfilled') {
|
|
47
|
+
setOrders(ordersResult.value.data);
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const message = err instanceof Error ? err.message : 'Failed to load account data.';
|
|
51
|
+
setError(message);
|
|
52
|
+
} finally {
|
|
53
|
+
setLoading(false);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
loadAccountData();
|
|
58
|
+
}, [isLoggedIn, authLoading, router]);
|
|
59
|
+
|
|
60
|
+
if (authLoading || !isLoggedIn) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (loading) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex min-h-[60vh] items-center justify-center">
|
|
67
|
+
<LoadingSpinner size="lg" />
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (error && !profile) {
|
|
73
|
+
return (
|
|
74
|
+
<div className="mx-auto max-w-3xl px-4 py-16 text-center sm:px-6 lg:px-8">
|
|
75
|
+
<h1 className="text-foreground text-2xl font-bold">{tc('error')}</h1>
|
|
76
|
+
<p className="text-muted-foreground mt-2">{error}</p>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={() => window.location.reload()}
|
|
80
|
+
className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
|
|
81
|
+
>
|
|
82
|
+
{tc('tryAgain')}
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
|
|
90
|
+
<div className="mb-6 flex items-center justify-between">
|
|
91
|
+
<h1 className="text-foreground text-2xl font-bold">{t('myAccount')}</h1>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={logout}
|
|
95
|
+
className="text-muted-foreground hover:text-foreground text-sm transition-colors"
|
|
96
|
+
>
|
|
97
|
+
{t('signOut')}
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Profile Section */}
|
|
102
|
+
{profile && (
|
|
103
|
+
<ProfileSection profile={profile} onProfileUpdate={setProfile} className="mb-6" />
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{/* Address Book */}
|
|
107
|
+
{profile && (
|
|
108
|
+
<AddressBook
|
|
109
|
+
addresses={profile.addresses}
|
|
110
|
+
onUpdate={(updated) => setProfile((p) => (p ? { ...p, addresses: updated } : p))}
|
|
111
|
+
className="mb-8"
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{/* Order History */}
|
|
116
|
+
<div>
|
|
117
|
+
<h2 className="text-foreground mb-4 text-lg font-semibold">{t('orderHistory')}</h2>
|
|
118
|
+
<OrderHistory orders={orders} />
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|