create-brainerce-store 1.9.1 → 1.11.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.
@@ -1,96 +1,102 @@
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 tp = useTranslations('productDetail');
21
+ const { price, originalPrice, isOnSale } = getProductPriceInfo(product);
22
+ const mainImage = product.images?.[0];
23
+ const imageUrl = mainImage?.url || null;
24
+ const slug = product.slug || product.id;
25
+
26
+ return (
27
+ <Link
28
+ href={`/products/${slug}`}
29
+ className={cn(
30
+ 'border-border bg-background group block overflow-hidden rounded-lg border transition-shadow hover:shadow-md',
31
+ className
32
+ )}
33
+ >
34
+ {/* Image */}
35
+ <div className="bg-muted relative aspect-square overflow-hidden">
36
+ {imageUrl ? (
37
+ <Image
38
+ src={imageUrl}
39
+ alt={mainImage?.alt || product.name}
40
+ fill
41
+ sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
42
+ className="object-cover transition-transform duration-300 group-hover:scale-105"
43
+ />
44
+ ) : (
45
+ <div className="text-muted-foreground absolute inset-0 flex items-center justify-center">
46
+ <svg className="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
47
+ <path
48
+ strokeLinecap="round"
49
+ strokeLinejoin="round"
50
+ strokeWidth={1.5}
51
+ 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"
52
+ />
53
+ </svg>
54
+ </div>
55
+ )}
56
+
57
+ {/* Badges */}
58
+ <div className="absolute start-2 top-2 flex flex-col gap-1">
59
+ {isOnSale && (
60
+ <span className="bg-destructive text-destructive-foreground rounded px-2 py-1 text-xs font-bold">
61
+ {t('sale')}
62
+ </span>
63
+ )}
64
+ <DiscountBadge discount={product.discount} />
65
+ {product.isDownloadable && (
66
+ <span className="bg-primary text-primary-foreground rounded px-2 py-1 text-xs font-bold">
67
+ {tp('digitalProduct')}
68
+ </span>
69
+ )}
70
+ </div>
71
+ </div>
72
+
73
+ {/* Content */}
74
+ <div className="space-y-2 p-3">
75
+ {/* Categories */}
76
+ {product.categories && product.categories.length > 0 && (
77
+ <div className="flex flex-wrap gap-1">
78
+ {product.categories.slice(0, 2).map((cat) => (
79
+ <span
80
+ key={cat.id}
81
+ className="text-muted-foreground bg-muted rounded px-1.5 py-0.5 text-[10px]"
82
+ >
83
+ {cat.name}
84
+ </span>
85
+ ))}
86
+ </div>
87
+ )}
88
+
89
+ {/* Name */}
90
+ <h3 className="text-foreground group-hover:text-primary line-clamp-2 text-sm font-medium transition-colors">
91
+ {product.name}
92
+ </h3>
93
+
94
+ {/* Price */}
95
+ <PriceDisplay price={originalPrice} salePrice={isOnSale ? price : undefined} size="sm" />
96
+
97
+ {/* Stock */}
98
+ <StockBadge inventory={product.inventory} />
99
+ </div>
100
+ </Link>
101
+ );
102
+ }
@@ -1,110 +1,107 @@
1
- 'use client';
2
-
3
- import { useEffect, useState } from 'react';
4
- import Link from 'next/link';
5
- import Image from 'next/image';
6
- import type { ProductRecommendation } from 'brainerce';
7
- import { PriceDisplay } from '@/components/shared/price-display';
8
- import { cn } from '@/lib/utils';
9
-
10
- interface RecommendationCardProps {
11
- item: ProductRecommendation;
12
- className?: string;
13
- }
14
-
15
- function RecommendationCard({ item, className }: RecommendationCardProps) {
16
- const imageUrl = item.images?.[0]?.url || null;
17
- const slug = item.slug || item.id;
18
- const basePrice = parseFloat(item.basePrice);
19
- const salePrice = item.salePrice ? parseFloat(item.salePrice) : undefined;
20
- const isOnSale = salePrice != null && salePrice < basePrice;
21
-
22
- return (
23
- <Link
24
- href={`/products/${slug}`}
25
- className={cn(
26
- 'border-border bg-background group block overflow-hidden rounded-lg border transition-shadow hover:shadow-md',
27
- className
28
- )}
29
- >
30
- <div className="bg-muted relative aspect-square overflow-hidden">
31
- {imageUrl ? (
32
- <Image
33
- src={imageUrl}
34
- alt={item.name}
35
- fill
36
- sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 20vw"
37
- className="object-cover transition-transform duration-300 group-hover:scale-105"
38
- />
39
- ) : (
40
- <div className="text-muted-foreground absolute inset-0 flex items-center justify-center">
41
- <svg className="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
42
- <path
43
- strokeLinecap="round"
44
- strokeLinejoin="round"
45
- strokeWidth={1.5}
46
- 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"
47
- />
48
- </svg>
49
- </div>
50
- )}
51
- </div>
52
- <div className="space-y-1.5 p-3">
53
- <h3 className="text-foreground group-hover:text-primary line-clamp-2 text-sm font-medium transition-colors">
54
- {item.name}
55
- </h3>
56
- <PriceDisplay
57
- price={basePrice}
58
- salePrice={isOnSale ? salePrice : undefined}
59
- size="sm"
60
- />
61
- </div>
62
- </Link>
63
- );
64
- }
65
-
66
- interface RecommendationSectionProps {
67
- title: string;
68
- items: ProductRecommendation[];
69
- className?: string;
70
- }
71
-
72
- export function RecommendationSection({ title, items, className }: RecommendationSectionProps) {
73
- if (items.length === 0) return null;
74
-
75
- return (
76
- <div className={cn('', className)}>
77
- <h2 className="text-foreground mb-4 text-xl font-semibold">{title}</h2>
78
- <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
79
- {items.map((item) => (
80
- <RecommendationCard key={item.id} item={item} />
81
- ))}
82
- </div>
83
- </div>
84
- );
85
- }
86
-
87
- interface CartRecommendationSectionProps {
88
- title: string;
89
- items: ProductRecommendation[];
90
- className?: string;
91
- }
92
-
93
- export function CartRecommendationSection({
94
- title,
95
- items,
96
- className,
97
- }: CartRecommendationSectionProps) {
98
- if (items.length === 0) return null;
99
-
100
- return (
101
- <div className={cn('', className)}>
102
- <h2 className="text-foreground mb-4 text-lg font-semibold">{title}</h2>
103
- <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
104
- {items.map((item) => (
105
- <RecommendationCard key={item.id} item={item} />
106
- ))}
107
- </div>
108
- </div>
109
- );
110
- }
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import Link from 'next/link';
5
+ import Image from 'next/image';
6
+ import type { ProductRecommendation } from 'brainerce';
7
+ import { PriceDisplay } from '@/components/shared/price-display';
8
+ import { cn } from '@/lib/utils';
9
+
10
+ interface RecommendationCardProps {
11
+ item: ProductRecommendation;
12
+ className?: string;
13
+ }
14
+
15
+ function RecommendationCard({ item, className }: RecommendationCardProps) {
16
+ const firstImage = item.images?.[0];
17
+ const imageUrl = typeof firstImage === 'string' ? firstImage : firstImage?.url || null;
18
+ const slug = item.slug || item.id;
19
+ const basePrice = parseFloat(item.basePrice);
20
+ const salePrice = item.salePrice ? parseFloat(item.salePrice) : undefined;
21
+ const isOnSale = salePrice != null && salePrice < basePrice;
22
+
23
+ return (
24
+ <Link
25
+ href={`/products/${slug}`}
26
+ className={cn(
27
+ 'border-border bg-background group block overflow-hidden rounded-lg border transition-shadow hover:shadow-md',
28
+ className
29
+ )}
30
+ >
31
+ <div className="bg-muted relative aspect-square overflow-hidden">
32
+ {imageUrl ? (
33
+ <Image
34
+ src={imageUrl}
35
+ alt={item.name}
36
+ fill
37
+ sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 20vw"
38
+ className="object-cover transition-transform duration-300 group-hover:scale-105"
39
+ />
40
+ ) : (
41
+ <div className="text-muted-foreground absolute inset-0 flex items-center justify-center">
42
+ <svg className="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
43
+ <path
44
+ strokeLinecap="round"
45
+ strokeLinejoin="round"
46
+ strokeWidth={1.5}
47
+ 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"
48
+ />
49
+ </svg>
50
+ </div>
51
+ )}
52
+ </div>
53
+ <div className="space-y-1.5 p-3">
54
+ <h3 className="text-foreground group-hover:text-primary line-clamp-2 text-sm font-medium transition-colors">
55
+ {item.name}
56
+ </h3>
57
+ <PriceDisplay price={basePrice} salePrice={isOnSale ? salePrice : undefined} size="sm" />
58
+ </div>
59
+ </Link>
60
+ );
61
+ }
62
+
63
+ interface RecommendationSectionProps {
64
+ title: string;
65
+ items: ProductRecommendation[];
66
+ className?: string;
67
+ }
68
+
69
+ export function RecommendationSection({ title, items, className }: RecommendationSectionProps) {
70
+ if (items.length === 0) return null;
71
+
72
+ return (
73
+ <div className={cn('', className)}>
74
+ <h2 className="text-foreground mb-4 text-xl font-semibold">{title}</h2>
75
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
76
+ {items.map((item) => (
77
+ <RecommendationCard key={item.id} item={item} />
78
+ ))}
79
+ </div>
80
+ </div>
81
+ );
82
+ }
83
+
84
+ interface CartRecommendationSectionProps {
85
+ title: string;
86
+ items: ProductRecommendation[];
87
+ className?: string;
88
+ }
89
+
90
+ export function CartRecommendationSection({
91
+ title,
92
+ items,
93
+ className,
94
+ }: CartRecommendationSectionProps) {
95
+ if (items.length === 0) return null;
96
+
97
+ return (
98
+ <div className={cn('', className)}>
99
+ <h2 className="text-foreground mb-4 text-lg font-semibold">{title}</h2>
100
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
101
+ {items.map((item) => (
102
+ <RecommendationCard key={item.id} item={item} />
103
+ ))}
104
+ </div>
105
+ </div>
106
+ );
107
+ }