create-brainerce-store 1.29.1 → 1.30.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/dist/index.js +1 -1
- package/messages/en.json +12 -1
- package/messages/he.json +12 -1
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +12 -5
- package/templates/nextjs/base/src/components/account/order-customizations.tsx +125 -0
- package/templates/nextjs/base/src/components/account/order-history.tsx +367 -350
- package/templates/nextjs/base/src/components/account/order-payment-block.tsx +58 -0
- package/templates/nextjs/base/src/components/account/order-shipping-block.tsx +79 -0
- package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +66 -0
- package/templates/nextjs/base/src/components/products/customization-fields.tsx +4 -4
- package/templates/nextjs/base/tsconfig.tsbuildinfo +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.
|
|
34
|
+
version: "1.30.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
package/messages/en.json
CHANGED
|
@@ -369,7 +369,18 @@
|
|
|
369
369
|
"region": "State / Region",
|
|
370
370
|
"postalCode": "Postal Code",
|
|
371
371
|
"country": "Country",
|
|
372
|
-
"isDefault": "Set as my default address"
|
|
372
|
+
"isDefault": "Set as my default address",
|
|
373
|
+
"customizations": "Customizations",
|
|
374
|
+
"shippingAddress": "Shipping address",
|
|
375
|
+
"tracking": "Tracking",
|
|
376
|
+
"trackOrder": "Track order",
|
|
377
|
+
"shippedOn": "Shipped on {date}",
|
|
378
|
+
"deliveredOn": "Delivered on {date}",
|
|
379
|
+
"paymentMethod": "Payment",
|
|
380
|
+
"paid": "Paid",
|
|
381
|
+
"refundedStatus": "Refunded",
|
|
382
|
+
"partiallyRefundedStatus": "Partially refunded",
|
|
383
|
+
"statusTimeline": "Timeline"
|
|
373
384
|
},
|
|
374
385
|
"checkoutAddress": {
|
|
375
386
|
"saveToProfile": "Save this address to my profile?",
|
package/messages/he.json
CHANGED
|
@@ -369,7 +369,18 @@
|
|
|
369
369
|
"region": "מדינה / אזור",
|
|
370
370
|
"postalCode": "מיקוד",
|
|
371
371
|
"country": "מדינה",
|
|
372
|
-
"isDefault": "הגדר ככתובת ברירת המחדל שלי"
|
|
372
|
+
"isDefault": "הגדר ככתובת ברירת המחדל שלי",
|
|
373
|
+
"customizations": "התאמות אישיות",
|
|
374
|
+
"shippingAddress": "כתובת משלוח",
|
|
375
|
+
"tracking": "מעקב",
|
|
376
|
+
"trackOrder": "מעקב הזמנה",
|
|
377
|
+
"shippedOn": "נשלח בתאריך {date}",
|
|
378
|
+
"deliveredOn": "נמסר בתאריך {date}",
|
|
379
|
+
"paymentMethod": "תשלום",
|
|
380
|
+
"paid": "שולם",
|
|
381
|
+
"refundedStatus": "הוחזר",
|
|
382
|
+
"partiallyRefundedStatus": "הוחזר חלקית",
|
|
383
|
+
"statusTimeline": "ציר זמן"
|
|
373
384
|
},
|
|
374
385
|
"checkoutAddress": {
|
|
375
386
|
"saveToProfile": "לשמור כתובת זו בפרופיל שלי?",
|
package/package.json
CHANGED
|
@@ -85,9 +85,13 @@ async function proxyRequest(
|
|
|
85
85
|
backendUrl.searchParams.set(key, value);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
// Build headers for backend request
|
|
88
|
+
// Build headers for backend request. Preserve the incoming Content-Type
|
|
89
|
+
// (including the multipart boundary for file uploads); default to JSON
|
|
90
|
+
// when absent. Overriding the Content-Type would strip the boundary and
|
|
91
|
+
// corrupt multipart uploads on the backend.
|
|
92
|
+
const incomingContentType = request.headers.get('content-type');
|
|
89
93
|
const headers: Record<string, string> = {
|
|
90
|
-
'Content-Type': 'application/json',
|
|
94
|
+
'Content-Type': incomingContentType || 'application/json',
|
|
91
95
|
};
|
|
92
96
|
|
|
93
97
|
// Send the proxy's own origin (not the client-supplied Origin header).
|
|
@@ -114,11 +118,14 @@ async function proxyRequest(
|
|
|
114
118
|
headers['Authorization'] = `Bearer ${tokenCookie.value}`;
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
// Forward request body for non-GET requests
|
|
118
|
-
|
|
121
|
+
// Forward request body for non-GET requests. Use ArrayBuffer so multipart
|
|
122
|
+
// bodies (file uploads) pass through unchanged — `request.text()` works for
|
|
123
|
+
// JSON but would break binary content.
|
|
124
|
+
const isMultipart = incomingContentType?.includes('multipart/form-data') ?? false;
|
|
125
|
+
let body: ArrayBuffer | string | undefined;
|
|
119
126
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
120
127
|
try {
|
|
121
|
-
body = await request.text();
|
|
128
|
+
body = isMultipart ? await request.arrayBuffer() : await request.text();
|
|
122
129
|
} catch {
|
|
123
130
|
// No body
|
|
124
131
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Image from 'next/image';
|
|
4
|
+
import type { OrderItem } from 'brainerce';
|
|
5
|
+
import { useTranslations } from '@/lib/translations';
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
type Customizations = NonNullable<OrderItem['customizations']>;
|
|
9
|
+
type CustomizationEntry = Customizations[string];
|
|
10
|
+
|
|
11
|
+
interface OrderCustomizationsProps {
|
|
12
|
+
customizations: OrderItem['customizations'];
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function OrderCustomizations({ customizations, className }: OrderCustomizationsProps) {
|
|
17
|
+
const t = useTranslations('account');
|
|
18
|
+
if (!customizations) return null;
|
|
19
|
+
const entries = Object.entries(customizations) as Array<[string, CustomizationEntry]>;
|
|
20
|
+
if (entries.length === 0) return null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<dl className={cn('bg-background/60 mt-2 space-y-1 rounded border-s-2 p-2 text-xs', className)}>
|
|
24
|
+
{entries.map(([key, entry]) => (
|
|
25
|
+
<div key={key} className="flex items-start gap-2">
|
|
26
|
+
<dt className="text-muted-foreground min-w-24 font-medium">{entry.label}:</dt>
|
|
27
|
+
<dd className="text-foreground flex-1 break-words">
|
|
28
|
+
<CustomizationValue entry={entry} fallback={t('productFallback')} />
|
|
29
|
+
</dd>
|
|
30
|
+
</div>
|
|
31
|
+
))}
|
|
32
|
+
</dl>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CustomizationValue({ entry, fallback }: { entry: CustomizationEntry; fallback: string }) {
|
|
37
|
+
const { type, value } = entry;
|
|
38
|
+
|
|
39
|
+
switch (type) {
|
|
40
|
+
case 'BOOLEAN':
|
|
41
|
+
return <span>{value === 'yes' || value === 'true' ? '✓' : '✗'}</span>;
|
|
42
|
+
|
|
43
|
+
case 'MULTI_SELECT':
|
|
44
|
+
return <span>{Array.isArray(value) ? value.join(', ') : String(value)}</span>;
|
|
45
|
+
|
|
46
|
+
case 'IMAGE': {
|
|
47
|
+
const url = typeof value === 'string' ? value : Array.isArray(value) ? value[0] : '';
|
|
48
|
+
if (!url) return <span className="text-muted-foreground">—</span>;
|
|
49
|
+
return (
|
|
50
|
+
<a href={url} target="_blank" rel="noopener noreferrer" className="inline-block">
|
|
51
|
+
<span className="relative inline-block h-12 w-12 overflow-hidden rounded border">
|
|
52
|
+
<Image src={url} alt={fallback} fill sizes="48px" className="object-cover" />
|
|
53
|
+
</span>
|
|
54
|
+
</a>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case 'GALLERY': {
|
|
59
|
+
const urls = Array.isArray(value) ? value : value ? [value] : [];
|
|
60
|
+
if (urls.length === 0) return <span className="text-muted-foreground">—</span>;
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex flex-wrap gap-1.5">
|
|
63
|
+
{urls.map((url, i) => (
|
|
64
|
+
<a
|
|
65
|
+
key={`${url}-${i}`}
|
|
66
|
+
href={url}
|
|
67
|
+
target="_blank"
|
|
68
|
+
rel="noopener noreferrer"
|
|
69
|
+
className="inline-block"
|
|
70
|
+
>
|
|
71
|
+
<span className="relative inline-block h-10 w-10 overflow-hidden rounded border">
|
|
72
|
+
<Image src={url} alt={fallback} fill sizes="40px" className="object-cover" />
|
|
73
|
+
</span>
|
|
74
|
+
</a>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case 'COLOR': {
|
|
81
|
+
const hex = typeof value === 'string' ? value : '';
|
|
82
|
+
return (
|
|
83
|
+
<span className="inline-flex items-center gap-1.5">
|
|
84
|
+
<span
|
|
85
|
+
className="inline-block h-4 w-4 rounded border"
|
|
86
|
+
style={{ backgroundColor: hex || 'transparent' }}
|
|
87
|
+
/>
|
|
88
|
+
<span className="font-mono">{hex || '—'}</span>
|
|
89
|
+
</span>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case 'DATE': {
|
|
94
|
+
const s = typeof value === 'string' ? value : '';
|
|
95
|
+
if (!s) return <span>—</span>;
|
|
96
|
+
const d = new Date(s);
|
|
97
|
+
return <span>{isNaN(d.getTime()) ? s : d.toLocaleDateString()}</span>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case 'DATETIME': {
|
|
101
|
+
const s = typeof value === 'string' ? value : '';
|
|
102
|
+
if (!s) return <span>—</span>;
|
|
103
|
+
const d = new Date(s);
|
|
104
|
+
return <span>{isNaN(d.getTime()) ? s : d.toLocaleString()}</span>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'URL': {
|
|
108
|
+
const href = typeof value === 'string' ? value : '';
|
|
109
|
+
if (!href) return <span>—</span>;
|
|
110
|
+
return (
|
|
111
|
+
<a
|
|
112
|
+
href={href}
|
|
113
|
+
target="_blank"
|
|
114
|
+
rel="noopener noreferrer"
|
|
115
|
+
className="text-primary break-all underline"
|
|
116
|
+
>
|
|
117
|
+
{href}
|
|
118
|
+
</a>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
default:
|
|
123
|
+
return <span>{Array.isArray(value) ? value.join(', ') : String(value ?? '')}</span>;
|
|
124
|
+
}
|
|
125
|
+
}
|