create-brainerce-store 1.4.1 → 1.5.1

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 (37) hide show
  1. package/dist/index.js +1 -1
  2. package/messages/en.json +9 -1
  3. package/messages/he.json +9 -1
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/src/app/account/page.tsx +8 -4
  6. package/templates/nextjs/base/src/app/auth/callback/page.tsx +90 -90
  7. package/templates/nextjs/base/src/app/cart/page.tsx +110 -110
  8. package/templates/nextjs/base/src/app/checkout/page.tsx +614 -614
  9. package/templates/nextjs/base/src/app/login/page.tsx +58 -58
  10. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +193 -193
  11. package/templates/nextjs/base/src/app/page.tsx +98 -98
  12. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +435 -435
  13. package/templates/nextjs/base/src/app/products/page.tsx +246 -246
  14. package/templates/nextjs/base/src/app/register/page.tsx +68 -68
  15. package/templates/nextjs/base/src/app/verify-email/page.tsx +293 -293
  16. package/templates/nextjs/base/src/components/account/order-history.tsx +198 -198
  17. package/templates/nextjs/base/src/components/account/profile-section.tsx +189 -40
  18. package/templates/nextjs/base/src/components/auth/login-form.tsx +94 -94
  19. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
  20. package/templates/nextjs/base/src/components/auth/register-form.tsx +184 -184
  21. package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -153
  22. package/templates/nextjs/base/src/components/cart/cart-summary.tsx +70 -70
  23. package/templates/nextjs/base/src/components/cart/coupon-input.tsx +134 -134
  24. package/templates/nextjs/base/src/components/cart/reservation-countdown.tsx +103 -103
  25. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +305 -305
  26. package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +64 -64
  27. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +350 -344
  28. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +199 -199
  29. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +110 -110
  30. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +65 -65
  31. package/templates/nextjs/base/src/components/layout/footer.tsx +38 -38
  32. package/templates/nextjs/base/src/components/layout/header.tsx +332 -332
  33. package/templates/nextjs/base/src/components/products/product-card.tsx +96 -96
  34. package/templates/nextjs/base/src/components/products/product-grid.tsx +35 -35
  35. package/templates/nextjs/base/src/components/shared/loading-spinner.tsx +32 -32
  36. package/templates/nextjs/base/src/lib/translations.ts +11 -11
  37. package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +5 -1
@@ -1,96 +1,96 @@
1
- 'use client';
2
-
3
- import Link from 'next/link';
4
- import Image from 'next/image';
5
- import type { Product } from 'brainerce';
6
- import { getProductPriceInfo } from 'brainerce';
7
- import { useTranslations } from '@/lib/translations';
8
- import { PriceDisplay } from '@/components/shared/price-display';
9
- import { StockBadge } from '@/components/products/stock-badge';
10
- import { DiscountBadge } from '@/components/products/discount-badge';
11
- import { cn } from '@/lib/utils';
12
-
13
- interface ProductCardProps {
14
- product: Product;
15
- className?: string;
16
- }
17
-
18
- export function ProductCard({ product, className }: ProductCardProps) {
19
- const t = useTranslations('common');
20
- const { price, originalPrice, isOnSale } = getProductPriceInfo(product);
21
- const mainImage = product.images?.[0];
22
- const imageUrl = mainImage?.url || null;
23
- const slug = product.slug || product.id;
24
-
25
- return (
26
- <Link
27
- href={`/products/${slug}`}
28
- className={cn(
29
- 'border-border bg-background group block overflow-hidden rounded-lg border transition-shadow hover:shadow-md',
30
- className
31
- )}
32
- >
33
- {/* Image */}
34
- <div className="bg-muted relative aspect-square overflow-hidden">
35
- {imageUrl ? (
36
- <Image
37
- src={imageUrl}
38
- alt={mainImage?.alt || product.name}
39
- fill
40
- sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
41
- className="object-cover transition-transform duration-300 group-hover:scale-105"
42
- />
43
- ) : (
44
- <div className="text-muted-foreground absolute inset-0 flex items-center justify-center">
45
- <svg className="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
46
- <path
47
- strokeLinecap="round"
48
- strokeLinejoin="round"
49
- strokeWidth={1.5}
50
- d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
51
- />
52
- </svg>
53
- </div>
54
- )}
55
-
56
- {/* Badges */}
57
- <div className="absolute start-2 top-2 flex flex-col gap-1">
58
- {isOnSale && (
59
- <span className="bg-destructive text-destructive-foreground rounded px-2 py-1 text-xs font-bold">
60
- {t('sale')}
61
- </span>
62
- )}
63
- <DiscountBadge discount={product.discount} />
64
- </div>
65
- </div>
66
-
67
- {/* Content */}
68
- <div className="space-y-2 p-3">
69
- {/* Categories */}
70
- {product.categories && product.categories.length > 0 && (
71
- <div className="flex flex-wrap gap-1">
72
- {product.categories.slice(0, 2).map((cat) => (
73
- <span
74
- key={cat.id}
75
- className="text-muted-foreground bg-muted rounded px-1.5 py-0.5 text-[10px]"
76
- >
77
- {cat.name}
78
- </span>
79
- ))}
80
- </div>
81
- )}
82
-
83
- {/* Name */}
84
- <h3 className="text-foreground group-hover:text-primary line-clamp-2 text-sm font-medium transition-colors">
85
- {product.name}
86
- </h3>
87
-
88
- {/* Price */}
89
- <PriceDisplay price={originalPrice} salePrice={isOnSale ? price : undefined} size="sm" />
90
-
91
- {/* Stock */}
92
- <StockBadge inventory={product.inventory} />
93
- </div>
94
- </Link>
95
- );
96
- }
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import Image from 'next/image';
5
+ import type { Product } from 'brainerce';
6
+ import { getProductPriceInfo } from 'brainerce';
7
+ import { useTranslations } from '@/lib/translations';
8
+ import { PriceDisplay } from '@/components/shared/price-display';
9
+ import { StockBadge } from '@/components/products/stock-badge';
10
+ import { DiscountBadge } from '@/components/products/discount-badge';
11
+ import { cn } from '@/lib/utils';
12
+
13
+ interface ProductCardProps {
14
+ product: Product;
15
+ className?: string;
16
+ }
17
+
18
+ export function ProductCard({ product, className }: ProductCardProps) {
19
+ const t = useTranslations('common');
20
+ const { price, originalPrice, isOnSale } = getProductPriceInfo(product);
21
+ const mainImage = product.images?.[0];
22
+ const imageUrl = mainImage?.url || null;
23
+ const slug = product.slug || product.id;
24
+
25
+ return (
26
+ <Link
27
+ href={`/products/${slug}`}
28
+ className={cn(
29
+ 'border-border bg-background group block overflow-hidden rounded-lg border transition-shadow hover:shadow-md',
30
+ className
31
+ )}
32
+ >
33
+ {/* Image */}
34
+ <div className="bg-muted relative aspect-square overflow-hidden">
35
+ {imageUrl ? (
36
+ <Image
37
+ src={imageUrl}
38
+ alt={mainImage?.alt || product.name}
39
+ fill
40
+ sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
41
+ className="object-cover transition-transform duration-300 group-hover:scale-105"
42
+ />
43
+ ) : (
44
+ <div className="text-muted-foreground absolute inset-0 flex items-center justify-center">
45
+ <svg className="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
46
+ <path
47
+ strokeLinecap="round"
48
+ strokeLinejoin="round"
49
+ strokeWidth={1.5}
50
+ d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
51
+ />
52
+ </svg>
53
+ </div>
54
+ )}
55
+
56
+ {/* Badges */}
57
+ <div className="absolute start-2 top-2 flex flex-col gap-1">
58
+ {isOnSale && (
59
+ <span className="bg-destructive text-destructive-foreground rounded px-2 py-1 text-xs font-bold">
60
+ {t('sale')}
61
+ </span>
62
+ )}
63
+ <DiscountBadge discount={product.discount} />
64
+ </div>
65
+ </div>
66
+
67
+ {/* Content */}
68
+ <div className="space-y-2 p-3">
69
+ {/* Categories */}
70
+ {product.categories && product.categories.length > 0 && (
71
+ <div className="flex flex-wrap gap-1">
72
+ {product.categories.slice(0, 2).map((cat) => (
73
+ <span
74
+ key={cat.id}
75
+ className="text-muted-foreground bg-muted rounded px-1.5 py-0.5 text-[10px]"
76
+ >
77
+ {cat.name}
78
+ </span>
79
+ ))}
80
+ </div>
81
+ )}
82
+
83
+ {/* Name */}
84
+ <h3 className="text-foreground group-hover:text-primary line-clamp-2 text-sm font-medium transition-colors">
85
+ {product.name}
86
+ </h3>
87
+
88
+ {/* Price */}
89
+ <PriceDisplay price={originalPrice} salePrice={isOnSale ? price : undefined} size="sm" />
90
+
91
+ {/* Stock */}
92
+ <StockBadge inventory={product.inventory} />
93
+ </div>
94
+ </Link>
95
+ );
96
+ }
@@ -1,35 +1,35 @@
1
- 'use client';
2
-
3
- import type { Product } from 'brainerce';
4
- import { useTranslations } from '@/lib/translations';
5
- import { ProductCard } from '@/components/products/product-card';
6
- import { cn } from '@/lib/utils';
7
-
8
- interface ProductGridProps {
9
- products: Product[];
10
- className?: string;
11
- }
12
-
13
- export function ProductGrid({ products, className }: ProductGridProps) {
14
- const t = useTranslations('products');
15
- if (products.length === 0) {
16
- return (
17
- <div className="py-16 text-center">
18
- <p className="text-muted-foreground text-lg">{t('noProducts')}</p>
19
- </div>
20
- );
21
- }
22
-
23
- return (
24
- <div
25
- className={cn(
26
- 'grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
27
- className
28
- )}
29
- >
30
- {products.map((product) => (
31
- <ProductCard key={product.id} product={product} />
32
- ))}
33
- </div>
34
- );
35
- }
1
+ 'use client';
2
+
3
+ import type { Product } from 'brainerce';
4
+ import { useTranslations } from '@/lib/translations';
5
+ import { ProductCard } from '@/components/products/product-card';
6
+ import { cn } from '@/lib/utils';
7
+
8
+ interface ProductGridProps {
9
+ products: Product[];
10
+ className?: string;
11
+ }
12
+
13
+ export function ProductGrid({ products, className }: ProductGridProps) {
14
+ const t = useTranslations('products');
15
+ if (products.length === 0) {
16
+ return (
17
+ <div className="py-16 text-center">
18
+ <p className="text-muted-foreground text-lg">{t('noProducts')}</p>
19
+ </div>
20
+ );
21
+ }
22
+
23
+ return (
24
+ <div
25
+ className={cn(
26
+ 'grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
27
+ className
28
+ )}
29
+ >
30
+ {products.map((product) => (
31
+ <ProductCard key={product.id} product={product} />
32
+ ))}
33
+ </div>
34
+ );
35
+ }
@@ -1,32 +1,32 @@
1
- 'use client';
2
-
3
- import { useTranslations } from '@/lib/translations';
4
- import { cn } from '@/lib/utils';
5
-
6
- interface LoadingSpinnerProps {
7
- size?: 'sm' | 'md' | 'lg';
8
- className?: string;
9
- }
10
-
11
- const sizeClasses = {
12
- sm: 'h-4 w-4 border-2',
13
- md: 'h-8 w-8 border-2',
14
- lg: 'h-12 w-12 border-3',
15
- };
16
-
17
- export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
18
- const t = useTranslations('common');
19
- return (
20
- <div
21
- className={cn(
22
- 'border-muted-foreground/30 border-t-primary animate-spin rounded-full',
23
- sizeClasses[size],
24
- className
25
- )}
26
- role="status"
27
- aria-label={t('loading')}
28
- >
29
- <span className="sr-only">{t('loading')}</span>
30
- </div>
31
- );
32
- }
1
+ 'use client';
2
+
3
+ import { useTranslations } from '@/lib/translations';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ interface LoadingSpinnerProps {
7
+ size?: 'sm' | 'md' | 'lg';
8
+ className?: string;
9
+ }
10
+
11
+ const sizeClasses = {
12
+ sm: 'h-4 w-4 border-2',
13
+ md: 'h-8 w-8 border-2',
14
+ lg: 'h-12 w-12 border-3',
15
+ };
16
+
17
+ export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
18
+ const t = useTranslations('common');
19
+ return (
20
+ <div
21
+ className={cn(
22
+ 'border-muted-foreground/30 border-t-primary animate-spin rounded-full',
23
+ sizeClasses[size],
24
+ className
25
+ )}
26
+ role="status"
27
+ aria-label={t('loading')}
28
+ >
29
+ <span className="sr-only">{t('loading')}</span>
30
+ </div>
31
+ );
32
+ }
@@ -1,11 +1,11 @@
1
- import { messages } from '@/i18n';
2
-
3
- type Messages = typeof messages;
4
- type Namespace = keyof Messages;
5
-
6
- export function useTranslations<N extends Namespace>(namespace: N) {
7
- const ns = messages[namespace] as Record<string, string>;
8
- return function t(key: keyof Messages[N]): string {
9
- return ns[key as string] || `${String(namespace)}.${key as string}`;
10
- };
11
- }
1
+ import { messages } from '@/i18n';
2
+
3
+ type Messages = typeof messages;
4
+ type Namespace = keyof Messages;
5
+
6
+ export function useTranslations<N extends Namespace>(namespace: N) {
7
+ const ns = messages[namespace] as Record<string, string>;
8
+ return function t(key: keyof Messages[N]): string {
9
+ return ns[key as string] || `${String(namespace)}.${key as string}`;
10
+ };
11
+ }
@@ -23,6 +23,7 @@ export function useStoreInfo() {
23
23
  // ---- Auth Context ----
24
24
  interface AuthContextValue {
25
25
  isLoggedIn: boolean;
26
+ authLoading: boolean;
26
27
  token: string | null;
27
28
  login: (token: string) => void;
28
29
  logout: () => void;
@@ -30,6 +31,7 @@ interface AuthContextValue {
30
31
 
31
32
  const AuthContext = createContext<AuthContextValue>({
32
33
  isLoggedIn: false,
34
+ authLoading: true,
33
35
  token: null,
34
36
  login: () => {},
35
37
  logout: () => {},
@@ -65,6 +67,7 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
65
67
  const [storeInfo, setStoreInfo] = useState<StoreInfo | null>(null);
66
68
  const [storeLoading, setStoreLoading] = useState(true);
67
69
  const [token, setToken] = useState<string | null>(null);
70
+ const [authLoading, setAuthLoading] = useState(true);
68
71
  const [cart, setCart] = useState<Cart | null>(null);
69
72
  const [cartLoading, setCartLoading] = useState(true);
70
73
 
@@ -75,6 +78,7 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
75
78
  if (stored) {
76
79
  setToken(stored);
77
80
  }
81
+ setAuthLoading(false);
78
82
 
79
83
  client
80
84
  .getStoreInfo()
@@ -134,7 +138,7 @@ export function StoreProvider({ children }: { children: React.ReactNode }) {
134
138
 
135
139
  return (
136
140
  <StoreInfoContext.Provider value={{ storeInfo, loading: storeLoading }}>
137
- <AuthContext.Provider value={{ isLoggedIn: !!token, token, login, logout }}>
141
+ <AuthContext.Provider value={{ isLoggedIn: !!token, authLoading, token, login, logout }}>
138
142
  <CartContext.Provider
139
143
  value={{ cart, cartLoading, refreshCart, itemCount, totals }}
140
144
  >