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.
Files changed (32) hide show
  1. package/dist/index.js +12 -1
  2. package/messages/en.json +382 -378
  3. package/messages/he.json +382 -378
  4. package/package.json +46 -46
  5. package/templates/nextjs/base/next.config.ts +31 -31
  6. package/templates/nextjs/base/src/app/account/page.tsx +1 -1
  7. package/templates/nextjs/base/src/app/auth/callback/page.tsx +2 -1
  8. package/templates/nextjs/base/src/app/cart/page.tsx +1 -1
  9. package/templates/nextjs/base/src/app/checkout/page.tsx +973 -972
  10. package/templates/nextjs/base/src/app/forgot-password/page.tsx +1 -1
  11. package/templates/nextjs/base/src/app/layout.tsx.ejs +6 -5
  12. package/templates/nextjs/base/src/app/login/page.tsx +1 -2
  13. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +271 -271
  14. package/templates/nextjs/base/src/app/page.tsx +1 -1
  15. package/templates/nextjs/base/src/app/payment-complete/page.tsx +59 -59
  16. package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +27 -12
  17. package/templates/nextjs/base/src/app/products/page.tsx +8 -3
  18. package/templates/nextjs/base/src/app/register/page.tsx +1 -2
  19. package/templates/nextjs/base/src/app/reset-password/page.tsx +1 -2
  20. package/templates/nextjs/base/src/app/verify-email/page.tsx +1 -2
  21. package/templates/nextjs/base/src/components/auth/login-form.tsx +1 -1
  22. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +258 -184
  23. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +592 -592
  24. package/templates/nextjs/base/src/components/layout/footer.tsx +1 -1
  25. package/templates/nextjs/base/src/components/layout/header.tsx +1 -2
  26. package/templates/nextjs/base/src/components/products/product-card.tsx +1 -2
  27. package/templates/nextjs/base/src/components/products/recommendation-section.tsx +1 -1
  28. package/templates/nextjs/base/src/lib/navigation.tsx.ejs +60 -0
  29. package/templates/nextjs/themes/luxury/globals.css +399 -399
  30. package/templates/nextjs/themes/luxury/theme.json +23 -23
  31. package/templates/nextjs/themes/playful/globals.css +400 -400
  32. 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: parseFloat(selectedVariant.salePrice || selectedVariant.price),
162
- originalPrice: parseFloat(selectedVariant.price),
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
- selectedVariant.salePrice != null &&
168
- parseFloat(selectedVariant.salePrice) < parseFloat(selectedVariant.price)
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, useRouter } from 'next/navigation';
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}>{b.name}</option>
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}>{tg.name}</option>
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 'next/navigation';
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 'next/navigation';
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 'next/navigation';
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 'next/link';
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';