create-brainerce-store 1.72.0 → 1.73.0

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.
@@ -6,7 +6,7 @@ import { getClient } from '@/core/lib/brainerce';
6
6
  import { checkAuthStatus } from '@/core/lib/auth';
7
7
  import { useTranslations } from '@/core/lib/translations';
8
8
  import type { MyProductReview, ProductReview, ReviewPhotoUpload } from 'brainerce';
9
- import { IconStar } from '@/ui/shared/icons';
9
+ import { IconPlus, IconStar } from '@/ui/shared/icons';
10
10
 
11
11
  interface ReviewFormProps {
12
12
  productId: string;
@@ -381,16 +381,36 @@ function ReviewEditor({
381
381
  </ul>
382
382
  )}
383
383
 
384
- <input
385
- type="file"
386
- accept="image/jpeg,image/png,image/webp,image/gif"
387
- multiple
388
- disabled={uploading || roomLeft <= 0}
389
- onChange={handleFiles}
390
- className="text-muted-foreground block text-sm"
391
- />
384
+ {/* A <label> wrapping a visually hidden input, not a bare file control.
385
+ The browser's own "Choose Files" button cannot be styled and looks
386
+ nothing like the rest of the form. Two things this has to do that the
387
+ native control did for free: `disabled` on a hidden input greys
388
+ NOTHING, so the label paints its own disabled state, and the line
389
+ underneath says WHY it is disabled rather than leaving a dead button.
390
+ The input stays a real input (no ref.click()) so keyboard and
391
+ assistive tech reach it, and focus-within restores the focus ring. */}
392
+ <label
393
+ className={`btn-outline btn-sm focus-within:ring-primary/40 inline-flex cursor-pointer items-center gap-2 focus-within:ring-2 ${
394
+ uploading || roomLeft <= 0 ? 'pointer-events-none opacity-50' : ''
395
+ }`}
396
+ >
397
+ <IconPlus size={16} />
398
+ {uploading ? t('photoUploading') : t('choosePhotos')}
399
+ <input
400
+ type="file"
401
+ accept="image/jpeg,image/png,image/webp,image/gif"
402
+ multiple
403
+ disabled={uploading || roomLeft <= 0}
404
+ onChange={handleFiles}
405
+ className="sr-only"
406
+ />
407
+ </label>
392
408
 
393
- {uploading && <p className="text-muted-foreground text-xs">{t('photoUploading')}</p>}
409
+ <p className="text-muted-foreground text-xs">
410
+ {roomLeft <= 0
411
+ ? t('photoLimitReached', { max: String(photos.maxPerReview) })
412
+ : t('photoFormats', { mb: String(Math.round(photos.maxBytes / (1024 * 1024))) })}
413
+ </p>
394
414
  {/* Said BEFORE they submit: a shopper who uploads, submits, and cannot
395
415
  find their photo will conclude the site is broken. */}
396
416
  {photos.requiresApproval && (
@@ -1,137 +1,137 @@
1
- 'use client';
2
-
3
- import { useState } from 'react';
4
- import { CdnImage as Image } from '@/ui/shared/cdn-image';
5
- import type { CartItem as CartItemType } from 'brainerce';
6
- import { getCartItemImage, formatPrice } from 'brainerce';
7
- import { getClient } from '@/core/lib/brainerce';
8
- import { useTranslations } from '@/core/lib/translations';
9
- import { useCurrency } from '@/core/lib/use-currency';
10
- import { LoadingSpinner } from '@/ui/shared/loading-spinner';
11
- import { cn } from '@/core/lib/utils';
12
-
13
- interface CartItemProps {
14
- item: CartItemType;
15
- onUpdate: () => void;
16
- className?: string;
17
- }
18
-
19
- /**
20
- * DESIGN ME — single cart line: image, name, variant, unit price, quantity
21
- * controls, remove, line total; the `item` prop comes from useCartPage().cart.
22
- *
23
- * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
24
- */
25
- export function CartItem({ item, onUpdate, className }: CartItemProps) {
26
- const t = useTranslations('common');
27
- const td = useTranslations('productDetail');
28
- const currency = useCurrency();
29
- const [updating, setUpdating] = useState(false);
30
- const [removing, setRemoving] = useState(false);
31
-
32
- const productName = item.product.name;
33
- const imageUrl = getCartItemImage(item);
34
- const variantName = item.variant?.name;
35
- const unitPrice = parseFloat(item.unitPrice);
36
- const lineTotal = unitPrice * item.quantity;
37
-
38
- // The server decides purchasability, per line. `isAvailable === false` means
39
- // this line blocks checkout until it is removed, whether the stock ran out
40
- // (a released reservation is the common cause) or the product/variant was
41
- // withdrawn from sale. Keep the badge when you redesign this line.
42
- const isUnavailable = item.isAvailable === false;
43
- const unavailableLabel = isUnavailable
44
- ? item.unavailableReason === 'OUT_OF_STOCK'
45
- ? td('outOfStock')
46
- : td('unavailable')
47
- : null;
48
-
49
- async function handleQuantityChange(newQuantity: number) {
50
- if (newQuantity < 1 || updating) return;
51
-
52
- try {
53
- setUpdating(true);
54
- const client = getClient();
55
- await client.smartUpdateCartItem(item.productId, newQuantity, item.variantId || undefined);
56
- onUpdate();
57
- } catch (err) {
58
- console.error('Failed to update quantity:', err);
59
- } finally {
60
- setUpdating(false);
61
- }
62
- }
63
-
64
- async function handleRemove() {
65
- if (removing) return;
66
-
67
- try {
68
- setRemoving(true);
69
- const client = getClient();
70
- await client.smartRemoveFromCart(item.productId, item.variantId || undefined);
71
- onUpdate();
72
- } catch (err) {
73
- console.error('Failed to remove item:', err);
74
- } finally {
75
- setRemoving(false);
76
- }
77
- }
78
-
79
- return (
80
- <div className={cn('flex items-start gap-4', className)}>
81
- {/* Image — `relative` + fixed box are layout-critical for next/image fill */}
82
- <div className="relative h-20 w-20">
83
- {imageUrl ? (
84
- <Image src={imageUrl} alt={productName} fill sizes="80px" />
85
- ) : (
86
- <span className="sr-only">{productName}</span>
87
- )}
88
- </div>
89
-
90
- {/* Details */}
91
- <div>
92
- <h3>{productName}</h3>
93
-
94
- {/* Availability badge. This line blocks checkout while it shows. */}
95
- {unavailableLabel && <span data-state="unavailable">{unavailableLabel}</span>}
96
-
97
- {/* Variant name */}
98
- {variantName && <p>{variantName}</p>}
99
-
100
- {/* Unit price */}
101
- <p>{formatPrice(unitPrice, { currency }) as string}</p>
102
-
103
- {/* Quantity controls */}
104
- <div className="flex items-center gap-3">
105
- <div className="flex items-center gap-2">
106
- <button
107
- type="button"
108
- onClick={() => handleQuantityChange(item.quantity - 1)}
109
- disabled={updating || item.quantity <= 1}
110
- aria-label={td('decreaseQuantity')}
111
- >
112
- -
113
- </button>
114
- <span aria-live="polite">
115
- {updating ? <LoadingSpinner size="sm" /> : item.quantity}
116
- </span>
117
- <button
118
- type="button"
119
- onClick={() => handleQuantityChange(item.quantity + 1)}
120
- disabled={updating || isUnavailable}
121
- aria-label={td('increaseQuantity')}
122
- >
123
- +
124
- </button>
125
- </div>
126
-
127
- <button type="button" onClick={handleRemove} disabled={removing}>
128
- {removing ? t('removing') : t('remove')}
129
- </button>
130
- </div>
131
- </div>
132
-
133
- {/* Line total */}
134
- <span>{formatPrice(lineTotal, { currency }) as string}</span>
135
- </div>
136
- );
137
- }
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { CdnImage as Image } from '@/ui/shared/cdn-image';
5
+ import type { CartItem as CartItemType } from 'brainerce';
6
+ import { getCartItemImage, formatPrice } from 'brainerce';
7
+ import { getClient } from '@/core/lib/brainerce';
8
+ import { useTranslations } from '@/core/lib/translations';
9
+ import { useCurrency } from '@/core/lib/use-currency';
10
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
11
+ import { cn } from '@/core/lib/utils';
12
+
13
+ interface CartItemProps {
14
+ item: CartItemType;
15
+ onUpdate: () => void;
16
+ className?: string;
17
+ }
18
+
19
+ /**
20
+ * DESIGN ME — single cart line: image, name, variant, unit price, quantity
21
+ * controls, remove, line total; the `item` prop comes from useCartPage().cart.
22
+ *
23
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
24
+ */
25
+ export function CartItem({ item, onUpdate, className }: CartItemProps) {
26
+ const t = useTranslations('common');
27
+ const td = useTranslations('productDetail');
28
+ const currency = useCurrency();
29
+ const [updating, setUpdating] = useState(false);
30
+ const [removing, setRemoving] = useState(false);
31
+
32
+ const productName = item.product.name;
33
+ const imageUrl = getCartItemImage(item);
34
+ const variantName = item.variant?.name;
35
+ const unitPrice = parseFloat(item.unitPrice);
36
+ const lineTotal = unitPrice * item.quantity;
37
+
38
+ // The server decides purchasability, per line. `isAvailable === false` means
39
+ // this line blocks checkout until it is removed, whether the stock ran out
40
+ // (a released reservation is the common cause) or the product/variant was
41
+ // withdrawn from sale. Keep the badge when you redesign this line.
42
+ const isUnavailable = item.isAvailable === false;
43
+ const unavailableLabel = isUnavailable
44
+ ? item.unavailableReason === 'OUT_OF_STOCK'
45
+ ? td('outOfStock')
46
+ : td('unavailable')
47
+ : null;
48
+
49
+ async function handleQuantityChange(newQuantity: number) {
50
+ if (newQuantity < 1 || updating) return;
51
+
52
+ try {
53
+ setUpdating(true);
54
+ const client = getClient();
55
+ await client.smartUpdateCartItem(item.productId, newQuantity, item.variantId || undefined);
56
+ onUpdate();
57
+ } catch (err) {
58
+ console.error('Failed to update quantity:', err);
59
+ } finally {
60
+ setUpdating(false);
61
+ }
62
+ }
63
+
64
+ async function handleRemove() {
65
+ if (removing) return;
66
+
67
+ try {
68
+ setRemoving(true);
69
+ const client = getClient();
70
+ await client.smartRemoveFromCart(item.productId, item.variantId || undefined);
71
+ onUpdate();
72
+ } catch (err) {
73
+ console.error('Failed to remove item:', err);
74
+ } finally {
75
+ setRemoving(false);
76
+ }
77
+ }
78
+
79
+ return (
80
+ <div className={cn('flex items-start gap-4', className)}>
81
+ {/* Image — `relative` + fixed box are layout-critical for next/image fill */}
82
+ <div className="relative h-20 w-20">
83
+ {imageUrl ? (
84
+ <Image src={imageUrl} alt={productName} fill sizes="80px" />
85
+ ) : (
86
+ <span className="sr-only">{productName}</span>
87
+ )}
88
+ </div>
89
+
90
+ {/* Details */}
91
+ <div>
92
+ <h3>{productName}</h3>
93
+
94
+ {/* Availability badge. This line blocks checkout while it shows. */}
95
+ {unavailableLabel && <span data-state="unavailable">{unavailableLabel}</span>}
96
+
97
+ {/* Variant name */}
98
+ {variantName && <p>{variantName}</p>}
99
+
100
+ {/* Unit price */}
101
+ <p>{formatPrice(unitPrice, { currency }) as string}</p>
102
+
103
+ {/* Quantity controls */}
104
+ <div className="flex items-center gap-3">
105
+ <div className="flex items-center gap-2">
106
+ <button
107
+ type="button"
108
+ onClick={() => handleQuantityChange(item.quantity - 1)}
109
+ disabled={updating || item.quantity <= 1}
110
+ aria-label={td('decreaseQuantity')}
111
+ >
112
+ -
113
+ </button>
114
+ <span aria-live="polite">
115
+ {updating ? <LoadingSpinner size="sm" /> : item.quantity}
116
+ </span>
117
+ <button
118
+ type="button"
119
+ onClick={() => handleQuantityChange(item.quantity + 1)}
120
+ disabled={updating || isUnavailable}
121
+ aria-label={td('increaseQuantity')}
122
+ >
123
+ +
124
+ </button>
125
+ </div>
126
+
127
+ <button type="button" onClick={handleRemove} disabled={removing}>
128
+ {removing ? t('removing') : t('remove')}
129
+ </button>
130
+ </div>
131
+ </div>
132
+
133
+ {/* Line total */}
134
+ <span>{formatPrice(lineTotal, { currency }) as string}</span>
135
+ </div>
136
+ );
137
+ }
@@ -1,140 +1,140 @@
1
- 'use client';
2
-
3
- import { Link } from '@/core/lib/navigation';
4
- import { useCartPage } from '@/core/hooks/use-cart-page';
5
- import { CartItem } from '@/ui/cart/cart-item';
6
- import { CartUpgradeBanner } from '@/ui/cart/cart-upgrade-banner';
7
- import { CartBundleOfferCard } from '@/ui/cart/cart-bundle-offer';
8
- import { CartSummary } from '@/ui/cart/cart-summary';
9
- import { CouponInput } from '@/ui/cart/coupon-input';
10
- import { CartNudges } from '@/ui/cart/cart-nudges';
11
- import { FreeShippingBar } from '@/ui/cart/free-shipping-bar';
12
- import { ReservationCountdown } from '@/ui/cart/reservation-countdown';
13
- import { CartRecommendationSection } from '@/ui/product/recommendation-section';
14
- import { LoadingSpinner } from '@/ui/shared/loading-spinner';
15
- import { useTranslations } from '@/core/lib/translations';
16
-
17
- export function CartView() {
18
- const t = useTranslations('cart');
19
- const tc = useTranslations('common');
20
- const tr = useTranslations('reservation');
21
- const {
22
- cart,
23
- cartLoading,
24
- refreshCart,
25
- itemCount,
26
- cartRecs,
27
- upgrades,
28
- bundles,
29
- reservationExpired,
30
- unavailableItems,
31
- canProceedToCheckout,
32
- onReservationExpired,
33
- } = useCartPage();
34
-
35
- if (cartLoading) {
36
- return <LoadingSpinner size="lg" />;
37
- }
38
-
39
- // Empty cart state
40
- if (!cart || cart.items.length === 0) {
41
- return (
42
- <section>
43
- {/* DESIGN ME — empty-cart state: message + continue-shopping CTA; state comes from useCartPage(). Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
44
- <h1>{t('emptyTitle')}</h1>
45
- <p>{t('emptySubtitle')}</p>
46
- <Link href="/products">{tc('continueShopping')}</Link>
47
- </section>
48
- );
49
- }
50
-
51
- return (
52
- <section>
53
- {/* DESIGN ME — cart page: items list, upgrade/bundle offers, coupon, summary sidebar, checkout CTA, cross-sells; everything comes from useCartPage(). Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
54
- <h1>
55
- {t('title')} ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
56
- </h1>
57
-
58
- {/* Reservation countdown. onExpire refreshes the cart and closes the
59
- checkout gate below. Passing it is what makes expiry mean anything. */}
60
- {cart.reservation?.hasReservation && (
61
- <ReservationCountdown reservation={cart.reservation} onExpire={onReservationExpired} />
62
- )}
63
-
64
- <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
65
- {/* Cart Items */}
66
- <div className="lg:col-span-2">
67
- {/* Nudges */}
68
- {cart.nudges && cart.nudges.length > 0 && <CartNudges nudges={cart.nudges} />}
69
-
70
- {/* Cart items */}
71
- <ul>
72
- {cart.items.map((item) => (
73
- <li key={item.id}>
74
- <CartItem item={item} onUpdate={refreshCart} />
75
- {upgrades?.upgrades?.[item.productId] && (
76
- <CartUpgradeBanner
77
- suggestion={upgrades.upgrades[item.productId]}
78
- cartItem={item}
79
- onUpgrade={refreshCart}
80
- />
81
- )}
82
- </li>
83
- ))}
84
- </ul>
85
-
86
- {/* Bundle offers */}
87
- {bundles?.bundles && bundles.bundles.length > 0 && (
88
- <section>
89
- <h3>{t('bundleOffers')}</h3>
90
- {bundles.bundles.map((offer) => (
91
- <CartBundleOfferCard
92
- key={offer.id}
93
- offer={offer}
94
- cartId={cart.id}
95
- onAdd={refreshCart}
96
- />
97
- ))}
98
- </section>
99
- )}
100
-
101
- {/* Coupon input */}
102
- <CouponInput cart={cart} onUpdate={refreshCart} />
103
- </div>
104
-
105
- {/* Summary sidebar */}
106
- <aside className="lg:col-span-1">
107
- <FreeShippingBar />
108
- <CartSummary />
109
-
110
- {/* Proceed to checkout. When the gate is closed this must be a real
111
- disabled control, never a styled-down link: an anchor ignores
112
- `disabled` and would still navigate. */}
113
- {canProceedToCheckout ? (
114
- <Link href="/checkout">{t('proceedToCheckout')}</Link>
115
- ) : (
116
- <>
117
- <button type="button" disabled>
118
- {t('proceedToCheckout')}
119
- </button>
120
- <p data-state="blocked">
121
- {unavailableItems.length > 0
122
- ? t('unavailableItemsHint')
123
- : reservationExpired
124
- ? tr('expiredHint')
125
- : null}
126
- </p>
127
- </>
128
- )}
129
-
130
- <Link href="/products">{tc('continueShopping')}</Link>
131
- </aside>
132
- </div>
133
-
134
- {/* Cross-sell recommendations */}
135
- {cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
136
- <CartRecommendationSection title={t('youMightAlsoNeed')} items={cartRecs.recommendations} />
137
- )}
138
- </section>
139
- );
140
- }
1
+ 'use client';
2
+
3
+ import { Link } from '@/core/lib/navigation';
4
+ import { useCartPage } from '@/core/hooks/use-cart-page';
5
+ import { CartItem } from '@/ui/cart/cart-item';
6
+ import { CartUpgradeBanner } from '@/ui/cart/cart-upgrade-banner';
7
+ import { CartBundleOfferCard } from '@/ui/cart/cart-bundle-offer';
8
+ import { CartSummary } from '@/ui/cart/cart-summary';
9
+ import { CouponInput } from '@/ui/cart/coupon-input';
10
+ import { CartNudges } from '@/ui/cart/cart-nudges';
11
+ import { FreeShippingBar } from '@/ui/cart/free-shipping-bar';
12
+ import { ReservationCountdown } from '@/ui/cart/reservation-countdown';
13
+ import { CartRecommendationSection } from '@/ui/product/recommendation-section';
14
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
15
+ import { useTranslations } from '@/core/lib/translations';
16
+
17
+ export function CartView() {
18
+ const t = useTranslations('cart');
19
+ const tc = useTranslations('common');
20
+ const tr = useTranslations('reservation');
21
+ const {
22
+ cart,
23
+ cartLoading,
24
+ refreshCart,
25
+ itemCount,
26
+ cartRecs,
27
+ upgrades,
28
+ bundles,
29
+ reservationExpired,
30
+ unavailableItems,
31
+ canProceedToCheckout,
32
+ onReservationExpired,
33
+ } = useCartPage();
34
+
35
+ if (cartLoading) {
36
+ return <LoadingSpinner size="lg" />;
37
+ }
38
+
39
+ // Empty cart state
40
+ if (!cart || cart.items.length === 0) {
41
+ return (
42
+ <section>
43
+ {/* DESIGN ME — empty-cart state: message + continue-shopping CTA; state comes from useCartPage(). Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
44
+ <h1>{t('emptyTitle')}</h1>
45
+ <p>{t('emptySubtitle')}</p>
46
+ <Link href="/products">{tc('continueShopping')}</Link>
47
+ </section>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <section>
53
+ {/* DESIGN ME — cart page: items list, upgrade/bundle offers, coupon, summary sidebar, checkout CTA, cross-sells; everything comes from useCartPage(). Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
54
+ <h1>
55
+ {t('title')} ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
56
+ </h1>
57
+
58
+ {/* Reservation countdown. onExpire refreshes the cart and closes the
59
+ checkout gate below. Passing it is what makes expiry mean anything. */}
60
+ {cart.reservation?.hasReservation && (
61
+ <ReservationCountdown reservation={cart.reservation} onExpire={onReservationExpired} />
62
+ )}
63
+
64
+ <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
65
+ {/* Cart Items */}
66
+ <div className="lg:col-span-2">
67
+ {/* Nudges */}
68
+ {cart.nudges && cart.nudges.length > 0 && <CartNudges nudges={cart.nudges} />}
69
+
70
+ {/* Cart items */}
71
+ <ul>
72
+ {cart.items.map((item) => (
73
+ <li key={item.id}>
74
+ <CartItem item={item} onUpdate={refreshCart} />
75
+ {upgrades?.upgrades?.[item.productId] && (
76
+ <CartUpgradeBanner
77
+ suggestion={upgrades.upgrades[item.productId]}
78
+ cartItem={item}
79
+ onUpgrade={refreshCart}
80
+ />
81
+ )}
82
+ </li>
83
+ ))}
84
+ </ul>
85
+
86
+ {/* Bundle offers */}
87
+ {bundles?.bundles && bundles.bundles.length > 0 && (
88
+ <section>
89
+ <h3>{t('bundleOffers')}</h3>
90
+ {bundles.bundles.map((offer) => (
91
+ <CartBundleOfferCard
92
+ key={offer.id}
93
+ offer={offer}
94
+ cartId={cart.id}
95
+ onAdd={refreshCart}
96
+ />
97
+ ))}
98
+ </section>
99
+ )}
100
+
101
+ {/* Coupon input */}
102
+ <CouponInput cart={cart} onUpdate={refreshCart} />
103
+ </div>
104
+
105
+ {/* Summary sidebar */}
106
+ <aside className="lg:col-span-1">
107
+ <FreeShippingBar />
108
+ <CartSummary />
109
+
110
+ {/* Proceed to checkout. When the gate is closed this must be a real
111
+ disabled control, never a styled-down link: an anchor ignores
112
+ `disabled` and would still navigate. */}
113
+ {canProceedToCheckout ? (
114
+ <Link href="/checkout">{t('proceedToCheckout')}</Link>
115
+ ) : (
116
+ <>
117
+ <button type="button" disabled>
118
+ {t('proceedToCheckout')}
119
+ </button>
120
+ <p data-state="blocked">
121
+ {unavailableItems.length > 0
122
+ ? t('unavailableItemsHint')
123
+ : reservationExpired
124
+ ? tr('expiredHint')
125
+ : null}
126
+ </p>
127
+ </>
128
+ )}
129
+
130
+ <Link href="/products">{tc('continueShopping')}</Link>
131
+ </aside>
132
+ </div>
133
+
134
+ {/* Cross-sell recommendations */}
135
+ {cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
136
+ <CartRecommendationSection title={t('youMightAlsoNeed')} items={cartRecs.recommendations} />
137
+ )}
138
+ </section>
139
+ );
140
+ }