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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { Order } from 'brainerce';
|
|
4
|
+
import { useTranslations } from '@/lib/translations';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
interface OrderPaymentBlockProps {
|
|
8
|
+
order: Order;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FINANCIAL_STATUS_KEY: Record<
|
|
13
|
+
string,
|
|
14
|
+
'paid' | 'statusPending' | 'refundedStatus' | 'partiallyRefundedStatus'
|
|
15
|
+
> = {
|
|
16
|
+
paid: 'paid',
|
|
17
|
+
pending: 'statusPending',
|
|
18
|
+
refunded: 'refundedStatus',
|
|
19
|
+
partially_refunded: 'partiallyRefundedStatus',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const FINANCIAL_STATUS_COLOR: Record<string, string> = {
|
|
23
|
+
paid: 'bg-green-100 text-green-800 dark:bg-green-950/30 dark:text-green-400',
|
|
24
|
+
pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-950/30 dark:text-yellow-400',
|
|
25
|
+
refunded: 'bg-orange-100 text-orange-800 dark:bg-orange-950/30 dark:text-orange-400',
|
|
26
|
+
partially_refunded: 'bg-orange-100 text-orange-800 dark:bg-orange-950/30 dark:text-orange-400',
|
|
27
|
+
voided: 'bg-red-100 text-red-800 dark:bg-red-950/30 dark:text-red-400',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function OrderPaymentBlock({ order, className }: OrderPaymentBlockProps) {
|
|
31
|
+
const t = useTranslations('account');
|
|
32
|
+
if (!order.paymentMethod && !order.financialStatus) return null;
|
|
33
|
+
|
|
34
|
+
const statusKey = order.financialStatus ? FINANCIAL_STATUS_KEY[order.financialStatus] : null;
|
|
35
|
+
const statusLabel = statusKey ? t(statusKey) : order.financialStatus || '';
|
|
36
|
+
const statusClass = order.financialStatus
|
|
37
|
+
? FINANCIAL_STATUS_COLOR[order.financialStatus] || 'bg-muted text-muted-foreground'
|
|
38
|
+
: '';
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className={cn('border-border flex items-center gap-3 border-t pt-2 text-xs', className)}>
|
|
42
|
+
<p className="text-foreground text-sm font-medium">{t('paymentMethod')}</p>
|
|
43
|
+
{order.paymentMethod && (
|
|
44
|
+
<span className="text-foreground capitalize">{order.paymentMethod.replace(/_/g, ' ')}</span>
|
|
45
|
+
)}
|
|
46
|
+
{statusLabel && (
|
|
47
|
+
<span
|
|
48
|
+
className={cn(
|
|
49
|
+
'inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium',
|
|
50
|
+
statusClass
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
{statusLabel}
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { Order } from 'brainerce';
|
|
4
|
+
import { useTranslations } from '@/lib/translations';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
interface OrderShippingBlockProps {
|
|
8
|
+
order: Order;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function OrderShippingBlock({ order, className }: OrderShippingBlockProps) {
|
|
13
|
+
const t = useTranslations('account');
|
|
14
|
+
const addr = order.shippingAddress;
|
|
15
|
+
const hasAddress = !!addr?.line1;
|
|
16
|
+
const hasTracking = !!(order.trackingNumber || order.trackingUrl || order.carrier);
|
|
17
|
+
if (!hasAddress && !hasTracking) return null;
|
|
18
|
+
|
|
19
|
+
const fullName = [addr?.firstName, addr?.lastName].filter(Boolean).join(' ');
|
|
20
|
+
const cityLine = [addr?.city, addr?.state || addr?.region, addr?.postalCode]
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join(', ');
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className={cn('border-border space-y-1.5 border-t pt-2 text-xs', className)}>
|
|
26
|
+
{hasAddress && (
|
|
27
|
+
<>
|
|
28
|
+
<p className="text-foreground text-sm font-medium">{t('shippingAddress')}</p>
|
|
29
|
+
<address className="text-muted-foreground not-italic">
|
|
30
|
+
{fullName && <div className="text-foreground">{fullName}</div>}
|
|
31
|
+
{addr?.company && <div>{addr.company}</div>}
|
|
32
|
+
<div>{addr?.line1}</div>
|
|
33
|
+
{addr?.line2 && <div>{addr.line2}</div>}
|
|
34
|
+
{cityLine && <div>{cityLine}</div>}
|
|
35
|
+
{addr?.country && <div>{addr.country}</div>}
|
|
36
|
+
{addr?.phone && <div>{addr.phone}</div>}
|
|
37
|
+
</address>
|
|
38
|
+
</>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
{hasTracking && (
|
|
42
|
+
<div className={cn(hasAddress && 'mt-2')}>
|
|
43
|
+
<p className="text-foreground text-sm font-medium">{t('tracking')}</p>
|
|
44
|
+
<div className="text-muted-foreground space-y-0.5">
|
|
45
|
+
{order.carrier && order.trackingNumber && (
|
|
46
|
+
<div className="text-foreground font-mono">
|
|
47
|
+
{order.carrier} · {order.trackingNumber}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
{order.carrier && !order.trackingNumber && (
|
|
51
|
+
<div className="text-foreground">{order.carrier}</div>
|
|
52
|
+
)}
|
|
53
|
+
{!order.carrier && order.trackingNumber && (
|
|
54
|
+
<div className="text-foreground font-mono">{order.trackingNumber}</div>
|
|
55
|
+
)}
|
|
56
|
+
{order.shippedAt && (
|
|
57
|
+
<div>{t('shippedOn', { date: new Date(order.shippedAt).toLocaleDateString() })}</div>
|
|
58
|
+
)}
|
|
59
|
+
{order.deliveredAt && (
|
|
60
|
+
<div>
|
|
61
|
+
{t('deliveredOn', { date: new Date(order.deliveredAt).toLocaleDateString() })}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
{order.trackingUrl && (
|
|
65
|
+
<a
|
|
66
|
+
href={order.trackingUrl}
|
|
67
|
+
target="_blank"
|
|
68
|
+
rel="noopener noreferrer"
|
|
69
|
+
className="bg-primary text-primary-foreground mt-1 inline-block rounded px-2.5 py-1 text-xs font-medium hover:opacity-90"
|
|
70
|
+
>
|
|
71
|
+
{t('trackOrder')}
|
|
72
|
+
</a>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { Order, OrderStatus } from 'brainerce';
|
|
4
|
+
import { useTranslations } from '@/lib/translations';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
const STATUS_KEYS: Record<
|
|
8
|
+
OrderStatus,
|
|
9
|
+
| 'statusPending'
|
|
10
|
+
| 'statusProcessing'
|
|
11
|
+
| 'statusShipped'
|
|
12
|
+
| 'statusDelivered'
|
|
13
|
+
| 'statusCancelled'
|
|
14
|
+
| 'statusRefunded'
|
|
15
|
+
> = {
|
|
16
|
+
pending: 'statusPending',
|
|
17
|
+
processing: 'statusProcessing',
|
|
18
|
+
shipped: 'statusShipped',
|
|
19
|
+
delivered: 'statusDelivered',
|
|
20
|
+
cancelled: 'statusCancelled',
|
|
21
|
+
refunded: 'statusRefunded',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
interface OrderStatusTimelineProps {
|
|
25
|
+
history: Order['statusHistory'];
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function OrderStatusTimeline({ history, className }: OrderStatusTimelineProps) {
|
|
30
|
+
const t = useTranslations('account');
|
|
31
|
+
if (!history || history.length === 0) return null;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={cn('border-border border-t pt-2', className)}>
|
|
35
|
+
<p className="text-foreground mb-2 text-sm font-medium">{t('statusTimeline')}</p>
|
|
36
|
+
<ol className="space-y-1.5">
|
|
37
|
+
{history.map((entry, idx) => {
|
|
38
|
+
const key = STATUS_KEYS[entry.status] || 'statusPending';
|
|
39
|
+
const when = new Date(entry.at);
|
|
40
|
+
const whenStr = isNaN(when.getTime())
|
|
41
|
+
? entry.at
|
|
42
|
+
: when.toLocaleString(undefined, {
|
|
43
|
+
year: 'numeric',
|
|
44
|
+
month: 'short',
|
|
45
|
+
day: 'numeric',
|
|
46
|
+
hour: '2-digit',
|
|
47
|
+
minute: '2-digit',
|
|
48
|
+
});
|
|
49
|
+
return (
|
|
50
|
+
<li key={`${entry.status}-${idx}`} className="flex items-center gap-2 text-xs">
|
|
51
|
+
<span
|
|
52
|
+
className={cn(
|
|
53
|
+
'bg-primary inline-block h-2 w-2 flex-shrink-0 rounded-full',
|
|
54
|
+
idx === history.length - 1 ? 'opacity-100' : 'opacity-60'
|
|
55
|
+
)}
|
|
56
|
+
/>
|
|
57
|
+
<span className="text-foreground font-medium">{t(key)}</span>
|
|
58
|
+
<span className="text-muted-foreground">· {whenStr}</span>
|
|
59
|
+
{entry.note && <span className="text-muted-foreground truncate">— {entry.note}</span>}
|
|
60
|
+
</li>
|
|
61
|
+
);
|
|
62
|
+
})}
|
|
63
|
+
</ol>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -340,10 +340,10 @@ function SingleImageField({
|
|
|
340
340
|
className="text-foreground text-sm"
|
|
341
341
|
/>
|
|
342
342
|
{uploading && (
|
|
343
|
-
<
|
|
343
|
+
<span className="text-muted-foreground mt-2 inline-flex items-center gap-2 text-xs">
|
|
344
344
|
<LoadingSpinner size="sm" />
|
|
345
345
|
{t('uploading')}
|
|
346
|
-
</
|
|
346
|
+
</span>
|
|
347
347
|
)}
|
|
348
348
|
{value && !uploading && (
|
|
349
349
|
<div className="mt-2">
|
|
@@ -406,10 +406,10 @@ function GalleryField({
|
|
|
406
406
|
className="text-foreground text-sm"
|
|
407
407
|
/>
|
|
408
408
|
{uploading && (
|
|
409
|
-
<
|
|
409
|
+
<span className="text-muted-foreground mt-2 inline-flex items-center gap-2 text-xs">
|
|
410
410
|
<LoadingSpinner size="sm" />
|
|
411
411
|
{t('uploading')}
|
|
412
|
-
</
|
|
412
|
+
</span>
|
|
413
413
|
)}
|
|
414
414
|
{value.length > 0 && (
|
|
415
415
|
<div className="mt-2 flex flex-wrap gap-2">
|