create-brainerce-store 1.51.0 → 1.52.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.
- package/README.md +71 -0
- package/dist/index.js +24 -80
- package/messages/en.json +32 -0
- package/messages/he.json +32 -0
- package/package.json +1 -1
- package/templates/nextjs/base/.claude/commands/design.md +9 -0
- package/templates/nextjs/base/AI-GUIDE.md +52 -3
- package/templates/nextjs/base/CLAUDE.md +10 -0
- package/templates/nextjs/base/package.json.ejs +52 -39
- package/templates/nextjs/base/src/app/globals.css +36 -30
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +14 -10
- package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -0
- package/templates/nextjs/base/src/components/ui/badge.tsx +32 -0
- package/templates/nextjs/base/src/components/ui/button.tsx +49 -0
- package/templates/nextjs/base/src/components/ui/card.tsx +55 -0
- package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -0
- package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -0
- package/templates/nextjs/base/src/components/ui/input.tsx +22 -0
- package/templates/nextjs/base/src/components/ui/label.tsx +21 -0
- package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -0
- package/templates/nextjs/base/src/components/ui/select.tsx +153 -0
- package/templates/nextjs/base/src/components/ui/separator.tsx +26 -0
- package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -0
- package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -0
- package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -0
- package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -0
- package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -0
- package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +17 -11
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +2 -8
- package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -39
- package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +3 -1
- package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +8 -23
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +18 -27
- package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +10 -21
- package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +2 -3
- package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -103
- package/templates/nextjs/base/src/ui/home/hero-section.tsx +4 -6
- package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -125
- package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -103
- package/templates/nextjs/base/src/ui/layout/header-account.tsx +2 -17
- package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -70
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -164
- package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -182
- package/templates/nextjs/base/src/ui/product/customization-fields.tsx +456 -444
- package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -22
- package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +15 -27
- package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -175
- package/templates/nextjs/base/src/ui/product/price-display.tsx +3 -2
- package/templates/nextjs/base/src/ui/product/product-card.tsx +238 -271
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -470
- package/templates/nextjs/base/src/ui/product/product-listing.tsx +96 -79
- package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -107
- package/templates/nextjs/base/src/ui/product/review-form.tsx +44 -43
- package/templates/nextjs/{designs/passepartout → base/src}/ui/product/reviews-section.tsx.ejs +108 -96
- package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -63
- package/templates/nextjs/base/tailwind.config.ts +69 -42
- package/templates/nextjs/designs/{passepartout → atelier}/app-overlay/layout.tsx.ejs +23 -25
- package/templates/nextjs/designs/atelier/globals.css +396 -0
- package/templates/nextjs/designs/atelier/messages-patch/en.json +99 -0
- package/templates/nextjs/designs/atelier/messages-patch/he.json +99 -0
- package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-bundle-offer.tsx +110 -111
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -0
- package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-upgrade-banner.tsx +123 -141
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -0
- package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -0
- package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/free-shipping-bar.tsx +77 -62
- package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/reservation-countdown.tsx +89 -103
- package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -0
- package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +148 -0
- package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -0
- package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +73 -0
- package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -0
- package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +148 -0
- package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -0
- package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -0
- package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -0
- package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -0
- package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -0
- package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -0
- package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -0
- package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -0
- package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -0
- package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -0
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -0
- package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -0
- package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -0
- package/templates/nextjs/designs/{passepartout → atelier}/ui/product/discount-badge.tsx +17 -22
- package/templates/nextjs/designs/{passepartout → atelier}/ui/product/frequently-bought-together.tsx +196 -205
- package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -0
- package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -0
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +260 -0
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +471 -0
- package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +47 -0
- package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -0
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -0
- package/templates/nextjs/designs/{passepartout → atelier}/ui/product/review-form.tsx +267 -294
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -0
- package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -0
- package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -0
- package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -0
- package/templates/nextjs/designs/{passepartout → atelier}/ui/shared/fly-to-cart.ts +86 -85
- package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +269 -0
- package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +64 -0
- package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -0
- package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -0
- package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +2 -2
- package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +2 -0
- package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +1 -1
- package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +1 -1
- package/templates/nextjs/ui-canvas/home/hero-section.tsx +1 -1
- package/templates/nextjs/ui-canvas/home/home-client.tsx +1 -1
- package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +1 -1
- package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +2 -2
- package/templates/nextjs/ui-canvas/layout/header-account.tsx +1 -1
- package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +2 -0
- package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +2 -0
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +2 -2
- package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +2 -2
- package/templates/nextjs/ui-canvas/product/customization-fields.tsx +2 -0
- package/templates/nextjs/ui-canvas/product/discount-badge.tsx +2 -0
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +2 -0
- package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +2 -0
- package/templates/nextjs/ui-canvas/product/price-display.tsx +2 -0
- package/templates/nextjs/ui-canvas/product/product-card.tsx +1 -1
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +1 -1
- package/templates/nextjs/ui-canvas/product/product-grid.tsx +1 -1
- package/templates/nextjs/ui-canvas/product/product-listing.tsx +1 -1
- package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +4 -0
- package/templates/nextjs/ui-canvas/product/review-form.tsx +29 -32
- package/templates/nextjs/ui-canvas/product/{reviews-section.tsx → reviews-section.tsx.ejs} +26 -8
- package/templates/nextjs/ui-canvas/product/stock-badge.tsx +2 -0
- package/templates/nextjs/ui-canvas/product/variant-selector.tsx +2 -0
- package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +2 -0
- package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +2 -0
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx +0 -85
- package/templates/nextjs/designs/passepartout/globals.css +0 -412
- package/templates/nextjs/designs/passepartout/messages-patch/en.json +0 -87
- package/templates/nextjs/designs/passepartout/messages-patch/he.json +0 -87
- package/templates/nextjs/designs/passepartout/ui/cart/cart-nudges.tsx +0 -39
- package/templates/nextjs/designs/passepartout/ui/cart/cart-view.tsx +0 -149
- package/templates/nextjs/designs/passepartout/ui/home/home-client.tsx +0 -401
- package/templates/nextjs/designs/passepartout/ui/layout/announcement-bar.tsx +0 -139
- package/templates/nextjs/designs/passepartout/ui/layout/cart-link.tsx +0 -45
- package/templates/nextjs/designs/passepartout/ui/layout/faq-section.tsx +0 -103
- package/templates/nextjs/designs/passepartout/ui/layout/site-footer.tsx +0 -159
- package/templates/nextjs/designs/passepartout/ui/layout/site-header.tsx.ejs +0 -155
- package/templates/nextjs/designs/passepartout/ui/layout/trust-icons.tsx +0 -60
- package/templates/nextjs/designs/passepartout/ui/layout/whatsapp-button.tsx +0 -34
- package/templates/nextjs/designs/passepartout/ui/product/price-display.tsx +0 -111
- package/templates/nextjs/designs/passepartout/ui/product/product-card.tsx +0 -265
- package/templates/nextjs/designs/passepartout/ui/product/product-client-section.tsx +0 -667
- package/templates/nextjs/designs/passepartout/ui/product/product-grid.tsx +0 -35
- package/templates/nextjs/designs/passepartout/ui/product/product-listing.tsx +0 -427
- package/templates/nextjs/designs/passepartout/ui/product/recommendation-section.tsx +0 -113
- package/templates/nextjs/designs/passepartout/ui/product/stock-badge.tsx +0 -52
- package/templates/nextjs/designs/passepartout/ui/product/variant-selector.tsx +0 -307
- package/templates/nextjs/designs/passepartout/ui/shared/loading-spinner.tsx +0 -33
- package/templates/nextjs/themes/luxury/globals.css +0 -399
- package/templates/nextjs/themes/luxury/theme.json +0 -23
- package/templates/nextjs/themes/minimal/globals.css +0 -30
- package/templates/nextjs/themes/minimal/theme.json +0 -23
- package/templates/nextjs/themes/playful/globals.css +0 -400
- package/templates/nextjs/themes/playful/theme.json +0 -23
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useRef, useState } from 'react';
|
|
4
|
-
import { Link, useRouter } from '@/core/lib/navigation';
|
|
5
|
-
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
6
|
-
import type { Product } from 'brainerce';
|
|
7
|
-
import { getProductPriceInfo, getVariantPrice, formatPrice } from 'brainerce';
|
|
8
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
9
|
-
import { PriceDisplay } from '@/ui/product/price-display';
|
|
10
|
-
import { StockBadge } from '@/ui/product/stock-badge';
|
|
11
|
-
import { DiscountBadge } from '@/ui/product/discount-badge';
|
|
12
|
-
import { useCart } from '@/core/providers/store-provider';
|
|
13
|
-
import { useCurrency } from '@/core/lib/use-currency';
|
|
14
|
-
import { cn } from '@/core/lib/utils';
|
|
15
|
-
import { flyToCart } from '@/ui/shared/fly-to-cart';
|
|
16
|
-
|
|
17
|
-
interface ProductCardProps {
|
|
18
|
-
product: Product;
|
|
19
|
-
className?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Pick the price + currency to render for a given product (PRD §23 FX overlay).
|
|
24
|
-
*
|
|
25
|
-
* When the storefront passed `regionId` to `getProducts({ regionId })` AND the
|
|
26
|
-
* region currency differs from the store currency, the backend attaches
|
|
27
|
-
* additive `displayPrice` / `displayCurrency` fields. Otherwise we fall back
|
|
28
|
-
* to the canonical store-currency `basePrice` / `salePrice`. Either way the
|
|
29
|
-
* cart still charges in the store currency — this only affects display.
|
|
30
|
-
*/
|
|
31
|
-
function pickDisplayPrice(
|
|
32
|
-
product: Product,
|
|
33
|
-
fallbackCurrency: string
|
|
34
|
-
): { price: number | undefined; salePrice: number | null; currency: string } {
|
|
35
|
-
if (product.displayPrice != null && product.displayCurrency) {
|
|
36
|
-
const sale =
|
|
37
|
-
product.displaySalePrice != null ? parseFloat(product.displaySalePrice) : null;
|
|
38
|
-
return {
|
|
39
|
-
// `displayPrice` is the base price converted to the region currency —
|
|
40
|
-
// it goes into the PriceDisplay `price` (base) slot, NOT the sale slot.
|
|
41
|
-
price: parseFloat(product.displayPrice),
|
|
42
|
-
salePrice: sale != null && !Number.isNaN(sale) ? sale : null,
|
|
43
|
-
currency: product.displayCurrency,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
// Same-currency fallback. `getProductPriceInfo.price` is the EFFECTIVE
|
|
47
|
-
// charged amount (= salePrice when on sale, base otherwise); `originalPrice`
|
|
48
|
-
// is the base. Map them to PriceDisplay's (price = base, salePrice = sale).
|
|
49
|
-
const { price: effective, originalPrice, isOnSale } = getProductPriceInfo(product);
|
|
50
|
-
return {
|
|
51
|
-
price: originalPrice,
|
|
52
|
-
salePrice: isOnSale ? effective : null,
|
|
53
|
-
currency: fallbackCurrency,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function VariantPriceRange({ product }: { product: Product }) {
|
|
58
|
-
const currency = useCurrency();
|
|
59
|
-
|
|
60
|
-
let min: number;
|
|
61
|
-
let max: number;
|
|
62
|
-
if (product.priceMin && product.priceMax) {
|
|
63
|
-
min = parseFloat(product.priceMin);
|
|
64
|
-
max = parseFloat(product.priceMax);
|
|
65
|
-
} else {
|
|
66
|
-
const variants = product.variants ?? [];
|
|
67
|
-
if (variants.length === 0) return null;
|
|
68
|
-
const prices = variants.map((v) => getVariantPrice(v, product.basePrice));
|
|
69
|
-
min = Math.min(...prices);
|
|
70
|
-
max = Math.max(...prices);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (isNaN(min) || isNaN(max)) return null;
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<span className="text-foreground tabular text-sm">
|
|
77
|
-
{min === max
|
|
78
|
-
? (formatPrice(min, { currency }) as string)
|
|
79
|
-
: `${formatPrice(min, { currency })} – ${formatPrice(max, { currency })}`}
|
|
80
|
-
</span>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function ProductCard({ product, className }: ProductCardProps) {
|
|
85
|
-
const t = useTranslations('common');
|
|
86
|
-
const tp = useTranslations('productDetail');
|
|
87
|
-
const tProd = useTranslations('products');
|
|
88
|
-
const router = useRouter();
|
|
89
|
-
const { refreshCart } = useCart();
|
|
90
|
-
const fallbackCurrency = useCurrency();
|
|
91
|
-
// FX overlay (PRD §23): prefer the region-converted display values when the
|
|
92
|
-
// storefront passed regionId to getProducts. Otherwise fall back to the
|
|
93
|
-
// canonical store-currency basePrice / salePrice.
|
|
94
|
-
const display = pickDisplayPrice(product, fallbackCurrency);
|
|
95
|
-
const { price, originalPrice, isOnSale } = getProductPriceInfo(product);
|
|
96
|
-
const mainImage = product.images?.[0];
|
|
97
|
-
const imageUrl = mainImage?.url || null;
|
|
98
|
-
const slug = product.slug || product.id;
|
|
99
|
-
const isVariable = product.type === 'VARIABLE';
|
|
100
|
-
|
|
101
|
-
const [adding, setAdding] = useState(false);
|
|
102
|
-
const frameRef = useRef<HTMLDivElement>(null);
|
|
103
|
-
const [added, setAdded] = useState(false);
|
|
104
|
-
|
|
105
|
-
const canPurchase = product.inventory?.canPurchase !== false;
|
|
106
|
-
|
|
107
|
-
async function handleAddToCart(e: React.MouseEvent) {
|
|
108
|
-
e.preventDefault();
|
|
109
|
-
e.stopPropagation();
|
|
110
|
-
|
|
111
|
-
if (isVariable) {
|
|
112
|
-
router.push(`/products/${slug}`);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (adding || !canPurchase) return;
|
|
117
|
-
|
|
118
|
-
flyToCart(frameRef.current);
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
setAdding(true);
|
|
122
|
-
const { getClient } = await import('@/core/lib/brainerce');
|
|
123
|
-
const client = getClient();
|
|
124
|
-
await client.smartAddToCart({ productId: product.id, quantity: 1 });
|
|
125
|
-
await refreshCart();
|
|
126
|
-
setAdded(true);
|
|
127
|
-
setTimeout(() => setAdded(false), 2000);
|
|
128
|
-
} catch (err) {
|
|
129
|
-
console.error('Failed to add to cart:', err);
|
|
130
|
-
} finally {
|
|
131
|
-
setAdding(false);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return (
|
|
136
|
-
<div className={cn('group block', className)}>
|
|
137
|
-
{/* The framed work — pure paper mat behind the photo, 1px hairline frame */}
|
|
138
|
-
<Link href={`/products/${slug}`} className="block">
|
|
139
|
-
<div ref={frameRef} className="bg-surface border-border hung relative aspect-square overflow-hidden border">
|
|
140
|
-
{imageUrl ? (
|
|
141
|
-
<>
|
|
142
|
-
<Image
|
|
143
|
-
src={imageUrl}
|
|
144
|
-
alt={mainImage?.alt || product.name}
|
|
145
|
-
fill
|
|
146
|
-
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
|
147
|
-
className="object-cover transition-transform duration-700 group-hover:scale-[1.03]"
|
|
148
|
-
/>
|
|
149
|
-
{/* Second angle cross-fades in on hover — scale and fit at a glance */}
|
|
150
|
-
{product.images?.[1]?.url && (
|
|
151
|
-
<Image
|
|
152
|
-
src={product.images[1].url}
|
|
153
|
-
alt=""
|
|
154
|
-
fill
|
|
155
|
-
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
|
156
|
-
className="object-cover opacity-0 transition-opacity duration-500 group-hover:opacity-100"
|
|
157
|
-
/>
|
|
158
|
-
)}
|
|
159
|
-
</>
|
|
160
|
-
) : (
|
|
161
|
-
<div className="text-muted-foreground absolute inset-0 flex items-center justify-center">
|
|
162
|
-
<svg className="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
163
|
-
<path
|
|
164
|
-
strokeLinecap="round"
|
|
165
|
-
strokeLinejoin="round"
|
|
166
|
-
strokeWidth={1.5}
|
|
167
|
-
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"
|
|
168
|
-
/>
|
|
169
|
-
</svg>
|
|
170
|
-
</div>
|
|
171
|
-
)}
|
|
172
|
-
|
|
173
|
-
{/* Placard notes inside the frame — plain text on the mat, never boxes */}
|
|
174
|
-
<div className="absolute start-3 top-3 flex flex-col gap-1">
|
|
175
|
-
{isOnSale && (
|
|
176
|
-
<span className="text-destructive text-[0.6875rem] font-semibold tracking-[0.08em]">
|
|
177
|
-
{t('sale')}
|
|
178
|
-
</span>
|
|
179
|
-
)}
|
|
180
|
-
<DiscountBadge discount={product.discount} />
|
|
181
|
-
{product.isDownloadable && (
|
|
182
|
-
<span className="text-muted-foreground text-[0.6875rem] tracking-[0.08em]">
|
|
183
|
-
{tp('digitalProduct')}
|
|
184
|
-
</span>
|
|
185
|
-
)}
|
|
186
|
-
</div>
|
|
187
|
-
|
|
188
|
-
{/* Add-to-cart strip — slides up inside the frame; always visible on touch */}
|
|
189
|
-
{(isVariable || canPurchase) && (
|
|
190
|
-
<button
|
|
191
|
-
onClick={handleAddToCart}
|
|
192
|
-
disabled={adding}
|
|
193
|
-
aria-label={isVariable ? tProd('selectOptions') : tp('addToCart')}
|
|
194
|
-
className={cn(
|
|
195
|
-
'bg-surface/95 border-border absolute inset-x-0 bottom-0 border-t py-2.5 text-center text-xs font-semibold transition-all duration-200',
|
|
196
|
-
'translate-y-full opacity-0 group-hover:translate-y-0 group-hover:opacity-100',
|
|
197
|
-
'focus-visible:translate-y-0 focus-visible:opacity-100',
|
|
198
|
-
'[@media(hover:none)]:translate-y-0 [@media(hover:none)]:opacity-100',
|
|
199
|
-
'text-foreground'
|
|
200
|
-
)}
|
|
201
|
-
>
|
|
202
|
-
{added ? (
|
|
203
|
-
<span className="inline-flex items-center justify-center gap-1.5">
|
|
204
|
-
<svg
|
|
205
|
-
className="text-accent h-3.5 w-3.5"
|
|
206
|
-
fill="none"
|
|
207
|
-
viewBox="0 0 24 24"
|
|
208
|
-
stroke="currentColor"
|
|
209
|
-
strokeWidth={1.5}
|
|
210
|
-
aria-hidden="true"
|
|
211
|
-
>
|
|
212
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
213
|
-
</svg>
|
|
214
|
-
{tp('addedToCart')}
|
|
215
|
-
</span>
|
|
216
|
-
) : isVariable ? (
|
|
217
|
-
tProd('selectOptions')
|
|
218
|
-
) : (
|
|
219
|
-
tp('addToCart')
|
|
220
|
-
)}
|
|
221
|
-
</button>
|
|
222
|
-
)}
|
|
223
|
-
</div>
|
|
224
|
-
</Link>
|
|
225
|
-
|
|
226
|
-
{/* The placard below the frame */}
|
|
227
|
-
<div className="space-y-1.5 pt-3 text-start">
|
|
228
|
-
{/* Category — first one only, quiet caption */}
|
|
229
|
-
{product.categories && product.categories.length > 0 && (
|
|
230
|
-
<p className="text-muted-foreground text-[0.6875rem] tracking-[0.08em]">
|
|
231
|
-
{product.categories[0].name}
|
|
232
|
-
</p>
|
|
233
|
-
)}
|
|
234
|
-
|
|
235
|
-
{/* Name — clickable, underline draws from the right */}
|
|
236
|
-
<Link href={`/products/${slug}`} className="group/name block">
|
|
237
|
-
<h3 className="text-foreground line-clamp-2 font-serif text-[0.9375rem] font-normal">
|
|
238
|
-
<span className="relative">
|
|
239
|
-
{product.name}
|
|
240
|
-
<span
|
|
241
|
-
aria-hidden="true"
|
|
242
|
-
className="bg-foreground absolute -bottom-0.5 start-0 end-0 h-px origin-right scale-x-0 transition-transform duration-200 group-hover/name:scale-x-100"
|
|
243
|
-
/>
|
|
244
|
-
</span>
|
|
245
|
-
</h3>
|
|
246
|
-
</Link>
|
|
247
|
-
|
|
248
|
-
{/* Price */}
|
|
249
|
-
{isVariable ? (
|
|
250
|
-
<VariantPriceRange product={product} />
|
|
251
|
-
) : (
|
|
252
|
-
<PriceDisplay
|
|
253
|
-
price={display.price ?? originalPrice}
|
|
254
|
-
salePrice={display.salePrice ?? (isOnSale ? price : undefined)}
|
|
255
|
-
currency={display.currency}
|
|
256
|
-
size="sm"
|
|
257
|
-
/>
|
|
258
|
-
)}
|
|
259
|
-
|
|
260
|
-
{/* Stock */}
|
|
261
|
-
<StockBadge inventory={product.inventory} />
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
);
|
|
265
|
-
}
|