create-brainerce-store 1.27.4 → 1.27.6
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 +12 -1
- package/messages/en.json +382 -378
- package/messages/he.json +382 -378
- package/package.json +46 -46
- package/templates/nextjs/base/next.config.ts +31 -31
- package/templates/nextjs/base/src/app/account/page.tsx +1 -1
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +2 -1
- package/templates/nextjs/base/src/app/cart/page.tsx +1 -1
- package/templates/nextjs/base/src/app/checkout/page.tsx +973 -972
- package/templates/nextjs/base/src/app/forgot-password/page.tsx +1 -1
- package/templates/nextjs/base/src/app/layout.tsx.ejs +6 -5
- package/templates/nextjs/base/src/app/login/page.tsx +1 -2
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +271 -271
- package/templates/nextjs/base/src/app/page.tsx +1 -1
- package/templates/nextjs/base/src/app/payment-complete/page.tsx +59 -59
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +27 -12
- package/templates/nextjs/base/src/app/products/page.tsx +8 -3
- package/templates/nextjs/base/src/app/register/page.tsx +1 -2
- package/templates/nextjs/base/src/app/reset-password/page.tsx +1 -2
- package/templates/nextjs/base/src/app/verify-email/page.tsx +1 -2
- package/templates/nextjs/base/src/components/auth/login-form.tsx +1 -1
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +258 -184
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +592 -592
- package/templates/nextjs/base/src/components/layout/footer.tsx +1 -1
- package/templates/nextjs/base/src/components/layout/header.tsx +1 -2
- package/templates/nextjs/base/src/components/products/product-card.tsx +1 -2
- package/templates/nextjs/base/src/components/products/recommendation-section.tsx +1 -1
- package/templates/nextjs/base/src/lib/navigation.tsx.ejs +60 -0
- package/templates/nextjs/themes/luxury/globals.css +399 -399
- package/templates/nextjs/themes/luxury/theme.json +23 -23
- package/templates/nextjs/themes/playful/globals.css +400 -400
- package/templates/nextjs/themes/playful/theme.json +23 -23
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect } from 'react';
|
|
4
|
-
import { useSearchParams } from 'next/navigation';
|
|
5
|
-
import { Suspense } from 'react';
|
|
6
|
-
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Lightweight callback page for iframe-based payment providers (e.g. CardCom).
|
|
10
|
-
*
|
|
11
|
-
* After the customer pays on the provider's hosted page (rendered inside an
|
|
12
|
-
* iframe on the checkout page), the provider redirects *inside the iframe* to
|
|
13
|
-
* this page. We extract the relevant query params and send them to the parent
|
|
14
|
-
* window via postMessage so the checkout page can verify the payment
|
|
15
|
-
* server-side and proceed to order confirmation.
|
|
16
|
-
*/
|
|
17
|
-
function PaymentCompleteContent() {
|
|
18
|
-
const searchParams = useSearchParams();
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
// Only send postMessage when running inside an iframe
|
|
22
|
-
if (window.parent === window) {
|
|
23
|
-
// Not in iframe — fallback: redirect to order-confirmation directly
|
|
24
|
-
const checkoutId = searchParams.get('checkout_id');
|
|
25
|
-
if (checkoutId) {
|
|
26
|
-
window.location.href = `/order-confirmation?${searchParams.toString()}`;
|
|
27
|
-
}
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Collect all query params from the provider redirect
|
|
32
|
-
const data: Record<string, string> = {};
|
|
33
|
-
searchParams.forEach((value, key) => {
|
|
34
|
-
data[key] = value;
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
window.parent.postMessage({ type: 'brainerce:payment-complete', data }, window.location.origin);
|
|
38
|
-
}, [searchParams]);
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<div className="flex min-h-[200px] items-center justify-center">
|
|
42
|
-
<LoadingSpinner size="lg" />
|
|
43
|
-
</div>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export default function PaymentCompletePage() {
|
|
48
|
-
return (
|
|
49
|
-
<Suspense
|
|
50
|
-
fallback={
|
|
51
|
-
<div className="flex min-h-[200px] items-center justify-center">
|
|
52
|
-
<LoadingSpinner size="lg" />
|
|
53
|
-
</div>
|
|
54
|
-
}
|
|
55
|
-
>
|
|
56
|
-
<PaymentCompleteContent />
|
|
57
|
-
</Suspense>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
|
+
import { Suspense } from 'react';
|
|
6
|
+
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Lightweight callback page for iframe-based payment providers (e.g. CardCom).
|
|
10
|
+
*
|
|
11
|
+
* After the customer pays on the provider's hosted page (rendered inside an
|
|
12
|
+
* iframe on the checkout page), the provider redirects *inside the iframe* to
|
|
13
|
+
* this page. We extract the relevant query params and send them to the parent
|
|
14
|
+
* window via postMessage so the checkout page can verify the payment
|
|
15
|
+
* server-side and proceed to order confirmation.
|
|
16
|
+
*/
|
|
17
|
+
function PaymentCompleteContent() {
|
|
18
|
+
const searchParams = useSearchParams();
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
// Only send postMessage when running inside an iframe
|
|
22
|
+
if (window.parent === window) {
|
|
23
|
+
// Not in iframe — fallback: redirect to order-confirmation directly
|
|
24
|
+
const checkoutId = searchParams.get('checkout_id');
|
|
25
|
+
if (checkoutId) {
|
|
26
|
+
window.location.href = `/order-confirmation?${searchParams.toString()}`;
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Collect all query params from the provider redirect
|
|
32
|
+
const data: Record<string, string> = {};
|
|
33
|
+
searchParams.forEach((value, key) => {
|
|
34
|
+
data[key] = value;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
window.parent.postMessage({ type: 'brainerce:payment-complete', data }, window.location.origin);
|
|
38
|
+
}, [searchParams]);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex min-h-[200px] items-center justify-center">
|
|
42
|
+
<LoadingSpinner size="lg" />
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function PaymentCompletePage() {
|
|
48
|
+
return (
|
|
49
|
+
<Suspense
|
|
50
|
+
fallback={
|
|
51
|
+
<div className="flex min-h-[200px] items-center justify-center">
|
|
52
|
+
<LoadingSpinner size="lg" />
|
|
53
|
+
</div>
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
<PaymentCompleteContent />
|
|
57
|
+
</Suspense>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -157,20 +157,35 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
157
157
|
// Price info - use variant price if selected, else product price
|
|
158
158
|
const priceInfo = useMemo(() => {
|
|
159
159
|
if (selectedVariant?.price) {
|
|
160
|
+
const variantBase = parseFloat(selectedVariant.price);
|
|
161
|
+
const variantSale = selectedVariant.salePrice ? parseFloat(selectedVariant.salePrice) : null;
|
|
162
|
+
const variantEffective =
|
|
163
|
+
variantSale != null && variantSale < variantBase ? variantSale : variantBase;
|
|
164
|
+
|
|
165
|
+
// Overlay any product-level discount rule onto the variant price using the rule's ratio
|
|
166
|
+
if (product.discount) {
|
|
167
|
+
const ruleOriginal = parseFloat(product.discount.originalPrice) || 0;
|
|
168
|
+
const ruleDiscounted = parseFloat(product.discount.discountedPrice) || 0;
|
|
169
|
+
const ratio = ruleOriginal > 0 ? ruleDiscounted / ruleOriginal : 1;
|
|
170
|
+
const discounted = variantEffective * ratio;
|
|
171
|
+
const amount = Math.max(0, variantEffective - discounted);
|
|
172
|
+
return {
|
|
173
|
+
price: discounted,
|
|
174
|
+
originalPrice: variantEffective,
|
|
175
|
+
isOnSale: discounted < variantEffective,
|
|
176
|
+
discountAmount: amount,
|
|
177
|
+
discountPercent:
|
|
178
|
+
variantEffective > 0 ? Math.round((amount / variantEffective) * 100) : 0,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
160
182
|
return {
|
|
161
|
-
price:
|
|
162
|
-
originalPrice:
|
|
163
|
-
isOnSale:
|
|
164
|
-
selectedVariant.salePrice != null &&
|
|
165
|
-
parseFloat(selectedVariant.salePrice) < parseFloat(selectedVariant.price),
|
|
183
|
+
price: variantEffective,
|
|
184
|
+
originalPrice: variantBase,
|
|
185
|
+
isOnSale: variantEffective < variantBase,
|
|
166
186
|
discountPercent:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
? Math.round(
|
|
170
|
-
((parseFloat(selectedVariant.price) - parseFloat(selectedVariant.salePrice)) /
|
|
171
|
-
parseFloat(selectedVariant.price)) *
|
|
172
|
-
100
|
|
173
|
-
)
|
|
187
|
+
variantEffective < variantBase && variantBase > 0
|
|
188
|
+
? Math.round(((variantBase - variantEffective) / variantBase) * 100)
|
|
174
189
|
: 0,
|
|
175
190
|
};
|
|
176
191
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Suspense, useEffect, useState, useCallback, useRef } from 'react';
|
|
4
|
-
import { useSearchParams
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
|
+
import { useRouter } from '@/lib/navigation';
|
|
5
6
|
import type { Product } from 'brainerce';
|
|
6
7
|
import type { ProductQueryParams } from 'brainerce';
|
|
7
8
|
import { getClient } from '@/lib/brainerce';
|
|
@@ -378,7 +379,9 @@ function ProductsContent() {
|
|
|
378
379
|
>
|
|
379
380
|
<option value="">{t('allBrands')}</option>
|
|
380
381
|
{brands.map((b) => (
|
|
381
|
-
<option key={b.id} value={b.id}>
|
|
382
|
+
<option key={b.id} value={b.id}>
|
|
383
|
+
{b.name}
|
|
384
|
+
</option>
|
|
382
385
|
))}
|
|
383
386
|
</select>
|
|
384
387
|
)}
|
|
@@ -392,7 +395,9 @@ function ProductsContent() {
|
|
|
392
395
|
>
|
|
393
396
|
<option value="">{t('allTags')}</option>
|
|
394
397
|
{tags.map((tg) => (
|
|
395
|
-
<option key={tg.id} value={tg.id}>
|
|
398
|
+
<option key={tg.id} value={tg.id}>
|
|
399
|
+
{tg.name}
|
|
400
|
+
</option>
|
|
396
401
|
))}
|
|
397
402
|
</select>
|
|
398
403
|
)}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import { useRouter } from '
|
|
5
|
-
import Link from 'next/link';
|
|
4
|
+
import { useRouter, Link } from '@/lib/navigation';
|
|
6
5
|
import { useAuth } from '@/providers/store-provider';
|
|
7
6
|
import { proxyRegister } from '@/lib/auth';
|
|
8
7
|
import { RegisterForm } from '@/components/auth/register-form';
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import { useRouter } from '
|
|
5
|
-
import Link from 'next/link';
|
|
4
|
+
import { useRouter, Link } from '@/lib/navigation';
|
|
6
5
|
import { proxyResetPassword } from '@/lib/auth';
|
|
7
6
|
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
8
7
|
import { useTranslations } from '@/lib/translations';
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Suspense, useState, useRef, useEffect, useCallback } from 'react';
|
|
4
|
-
import { useRouter } from '
|
|
5
|
-
import Link from 'next/link';
|
|
4
|
+
import { useRouter, Link } from '@/lib/navigation';
|
|
6
5
|
import { useAuth } from '@/providers/store-provider';
|
|
7
6
|
import { proxyVerifyEmail, proxyResendVerification } from '@/lib/auth';
|
|
8
7
|
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import Link from '
|
|
4
|
+
import { Link } from '@/lib/navigation';
|
|
5
5
|
import { useTranslations } from '@/lib/translations';
|
|
6
6
|
import { cn } from '@/lib/utils';
|
|
7
7
|
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|