create-brainerce-store 1.30.0 → 1.31.2
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 +2 -2
- package/messages/en.json +434 -417
- package/messages/he.json +434 -417
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +242 -242
- package/templates/nextjs/base/src/app/contact/page.tsx +211 -0
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +566 -566
- package/templates/nextjs/base/src/components/account/order-customizations.tsx +125 -125
- package/templates/nextjs/base/src/components/account/order-history.tsx +367 -367
- package/templates/nextjs/base/src/components/account/order-payment-block.tsx +58 -58
- package/templates/nextjs/base/src/components/account/order-shipping-block.tsx +79 -79
- package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +66 -66
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +730 -730
- package/templates/nextjs/base/src/components/layout/footer.tsx +6 -0
- package/templates/nextjs/base/src/components/products/customization-fields.tsx +478 -478
- package/templates/nextjs/base/src/lib/safe-redirect.ts +60 -60
|
@@ -1,125 +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
|
-
}
|
|
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
|
+
}
|