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.
@@ -1,176 +1,176 @@
1
- 'use client';
2
-
3
- import { ShoppingBag } from 'lucide-react';
4
- import { Link } from '@/core/lib/navigation';
5
- import { useCartPage } from '@/core/hooks/use-cart-page';
6
- import { CartItem } from '@/ui/cart/cart-item';
7
- import { CartUpgradeBanner } from '@/ui/cart/cart-upgrade-banner';
8
- import { CartBundleOfferCard } from '@/ui/cart/cart-bundle-offer';
9
- import { CartSummary } from '@/ui/cart/cart-summary';
10
- import { CouponInput } from '@/ui/cart/coupon-input';
11
- import { CartNudges } from '@/ui/cart/cart-nudges';
12
- import { FreeShippingBar } from '@/ui/cart/free-shipping-bar';
13
- import { ReservationCountdown } from '@/ui/cart/reservation-countdown';
14
- import { CartRecommendationSection } from '@/ui/product/recommendation-section';
15
- import { LoadingSpinner } from '@/ui/shared/loading-spinner';
16
- import { Button } from '@/components/ui/button';
17
- import { Card } from '@/components/ui/card';
18
- import { Separator } from '@/components/ui/separator';
19
- import { useTranslations } from '@/core/lib/translations';
20
-
21
- export function CartView() {
22
- const t = useTranslations('cart');
23
- const tc = useTranslations('common');
24
- const tr = useTranslations('reservation');
25
- const {
26
- cart,
27
- cartLoading,
28
- refreshCart,
29
- itemCount,
30
- cartRecs,
31
- upgrades,
32
- bundles,
33
- reservationExpired,
34
- unavailableItems,
35
- canProceedToCheckout,
36
- onReservationExpired,
37
- } = useCartPage();
38
-
39
- if (cartLoading) {
40
- return (
41
- <div className="flex min-h-[60vh] items-center justify-center">
42
- <LoadingSpinner size="lg" />
43
- </div>
44
- );
45
- }
46
-
47
- // Empty cart state
48
- if (!cart || cart.items.length === 0) {
49
- return (
50
- <div className="mx-auto max-w-7xl px-4 py-16 text-center sm:px-6 lg:px-8">
51
- <ShoppingBag
52
- className="text-muted-foreground mx-auto mb-4 h-16 w-16"
53
- strokeWidth={1.5}
54
- aria-hidden="true"
55
- />
56
- <h1 className="text-foreground text-2xl font-bold">{t('emptyTitle')}</h1>
57
- <p className="text-muted-foreground mt-2">{t('emptySubtitle')}</p>
58
- <Button asChild size="lg" className="mt-6 rounded px-6 text-base">
59
- <Link href="/products">{tc('continueShopping')}</Link>
60
- </Button>
61
- </div>
62
- );
63
- }
64
-
65
- return (
66
- <div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
67
- <h1 className="text-foreground mb-6 text-2xl font-bold">
68
- {t('title')} ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
69
- </h1>
70
-
71
- {/* Reservation countdown. onExpire refreshes the cart and closes the
72
- checkout gate below. Passing it is what makes expiry mean anything. */}
73
- {cart.reservation?.hasReservation && (
74
- <ReservationCountdown
75
- reservation={cart.reservation}
76
- onExpire={onReservationExpired}
77
- className="mb-6"
78
- />
79
- )}
80
-
81
- <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
82
- {/* Cart Items */}
83
- <div className="lg:col-span-2">
84
- {/* Nudges */}
85
- {cart.nudges && cart.nudges.length > 0 && (
86
- <CartNudges nudges={cart.nudges} className="mb-4" />
87
- )}
88
-
89
- {/* Cart items */}
90
- <div>
91
- {cart.items.map((item) => (
92
- <div key={item.id}>
93
- <CartItem item={item} onUpdate={refreshCart} />
94
- {upgrades?.upgrades?.[item.productId] && (
95
- <CartUpgradeBanner
96
- suggestion={upgrades.upgrades[item.productId]}
97
- cartItem={item}
98
- onUpgrade={refreshCart}
99
- className="mb-2 ms-24"
100
- />
101
- )}
102
- </div>
103
- ))}
104
- </div>
105
-
106
- {/* Bundle offers */}
107
- {bundles?.bundles && bundles.bundles.length > 0 && (
108
- <div className="mt-6 space-y-3">
109
- <h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
110
- {bundles.bundles.map((offer) => (
111
- <CartBundleOfferCard
112
- key={offer.id}
113
- offer={offer}
114
- cartId={cart.id}
115
- onAdd={refreshCart}
116
- />
117
- ))}
118
- </div>
119
- )}
120
-
121
- {/* Coupon input */}
122
- <Separator className="mt-6" />
123
- <div className="pt-4">
124
- <CouponInput cart={cart} onUpdate={refreshCart} />
125
- </div>
126
- </div>
127
-
128
- {/* Summary sidebar */}
129
- <div className="lg:col-span-1">
130
- <Card className="bg-muted/50 sticky top-24 p-6 shadow-none">
131
- <FreeShippingBar className="mb-4" />
132
- <CartSummary />
133
-
134
- {/* Proceed to checkout. When the gate is closed this must be a
135
- real disabled control, never a styled-down link: an anchor
136
- ignores `disabled` and would still navigate. */}
137
- {canProceedToCheckout ? (
138
- <Button asChild size="lg" className="mt-6 w-full rounded px-6 text-sm">
139
- <Link href="/checkout">{t('proceedToCheckout')}</Link>
140
- </Button>
141
- ) : (
142
- <>
143
- <Button disabled size="lg" className="mt-6 w-full rounded px-6 text-sm">
144
- {t('proceedToCheckout')}
145
- </Button>
146
- <p className="text-destructive mt-2 text-xs">
147
- {unavailableItems.length > 0
148
- ? t('unavailableItemsHint')
149
- : reservationExpired
150
- ? tr('expiredHint')
151
- : null}
152
- </p>
153
- </>
154
- )}
155
-
156
- <Link
157
- href="/products"
158
- className="text-muted-foreground hover:text-foreground mt-3 inline-flex w-full items-center justify-center px-6 py-2 text-sm transition-colors"
159
- >
160
- {tc('continueShopping')}
161
- </Link>
162
- </Card>
163
- </div>
164
- </div>
165
-
166
- {/* Cross-sell recommendations */}
167
- {cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
168
- <CartRecommendationSection
169
- title={t('youMightAlsoNeed')}
170
- items={cartRecs.recommendations}
171
- className="mt-10"
172
- />
173
- )}
174
- </div>
175
- );
176
- }
1
+ 'use client';
2
+
3
+ import { ShoppingBag } from 'lucide-react';
4
+ import { Link } from '@/core/lib/navigation';
5
+ import { useCartPage } from '@/core/hooks/use-cart-page';
6
+ import { CartItem } from '@/ui/cart/cart-item';
7
+ import { CartUpgradeBanner } from '@/ui/cart/cart-upgrade-banner';
8
+ import { CartBundleOfferCard } from '@/ui/cart/cart-bundle-offer';
9
+ import { CartSummary } from '@/ui/cart/cart-summary';
10
+ import { CouponInput } from '@/ui/cart/coupon-input';
11
+ import { CartNudges } from '@/ui/cart/cart-nudges';
12
+ import { FreeShippingBar } from '@/ui/cart/free-shipping-bar';
13
+ import { ReservationCountdown } from '@/ui/cart/reservation-countdown';
14
+ import { CartRecommendationSection } from '@/ui/product/recommendation-section';
15
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
16
+ import { Button } from '@/components/ui/button';
17
+ import { Card } from '@/components/ui/card';
18
+ import { Separator } from '@/components/ui/separator';
19
+ import { useTranslations } from '@/core/lib/translations';
20
+
21
+ export function CartView() {
22
+ const t = useTranslations('cart');
23
+ const tc = useTranslations('common');
24
+ const tr = useTranslations('reservation');
25
+ const {
26
+ cart,
27
+ cartLoading,
28
+ refreshCart,
29
+ itemCount,
30
+ cartRecs,
31
+ upgrades,
32
+ bundles,
33
+ reservationExpired,
34
+ unavailableItems,
35
+ canProceedToCheckout,
36
+ onReservationExpired,
37
+ } = useCartPage();
38
+
39
+ if (cartLoading) {
40
+ return (
41
+ <div className="flex min-h-[60vh] items-center justify-center">
42
+ <LoadingSpinner size="lg" />
43
+ </div>
44
+ );
45
+ }
46
+
47
+ // Empty cart state
48
+ if (!cart || cart.items.length === 0) {
49
+ return (
50
+ <div className="mx-auto max-w-7xl px-4 py-16 text-center sm:px-6 lg:px-8">
51
+ <ShoppingBag
52
+ className="text-muted-foreground mx-auto mb-4 h-16 w-16"
53
+ strokeWidth={1.5}
54
+ aria-hidden="true"
55
+ />
56
+ <h1 className="text-foreground text-2xl font-bold">{t('emptyTitle')}</h1>
57
+ <p className="text-muted-foreground mt-2">{t('emptySubtitle')}</p>
58
+ <Button asChild size="lg" className="mt-6 rounded px-6 text-base">
59
+ <Link href="/products">{tc('continueShopping')}</Link>
60
+ </Button>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
67
+ <h1 className="text-foreground mb-6 text-2xl font-bold">
68
+ {t('title')} ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
69
+ </h1>
70
+
71
+ {/* Reservation countdown. onExpire refreshes the cart and closes the
72
+ checkout gate below. Passing it is what makes expiry mean anything. */}
73
+ {cart.reservation?.hasReservation && (
74
+ <ReservationCountdown
75
+ reservation={cart.reservation}
76
+ onExpire={onReservationExpired}
77
+ className="mb-6"
78
+ />
79
+ )}
80
+
81
+ <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
82
+ {/* Cart Items */}
83
+ <div className="lg:col-span-2">
84
+ {/* Nudges */}
85
+ {cart.nudges && cart.nudges.length > 0 && (
86
+ <CartNudges nudges={cart.nudges} className="mb-4" />
87
+ )}
88
+
89
+ {/* Cart items */}
90
+ <div>
91
+ {cart.items.map((item) => (
92
+ <div key={item.id}>
93
+ <CartItem item={item} onUpdate={refreshCart} />
94
+ {upgrades?.upgrades?.[item.productId] && (
95
+ <CartUpgradeBanner
96
+ suggestion={upgrades.upgrades[item.productId]}
97
+ cartItem={item}
98
+ onUpgrade={refreshCart}
99
+ className="mb-2 ms-24"
100
+ />
101
+ )}
102
+ </div>
103
+ ))}
104
+ </div>
105
+
106
+ {/* Bundle offers */}
107
+ {bundles?.bundles && bundles.bundles.length > 0 && (
108
+ <div className="mt-6 space-y-3">
109
+ <h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
110
+ {bundles.bundles.map((offer) => (
111
+ <CartBundleOfferCard
112
+ key={offer.id}
113
+ offer={offer}
114
+ cartId={cart.id}
115
+ onAdd={refreshCart}
116
+ />
117
+ ))}
118
+ </div>
119
+ )}
120
+
121
+ {/* Coupon input */}
122
+ <Separator className="mt-6" />
123
+ <div className="pt-4">
124
+ <CouponInput cart={cart} onUpdate={refreshCart} />
125
+ </div>
126
+ </div>
127
+
128
+ {/* Summary sidebar */}
129
+ <div className="lg:col-span-1">
130
+ <Card className="bg-muted/50 sticky top-24 p-6 shadow-none">
131
+ <FreeShippingBar className="mb-4" />
132
+ <CartSummary />
133
+
134
+ {/* Proceed to checkout. When the gate is closed this must be a
135
+ real disabled control, never a styled-down link: an anchor
136
+ ignores `disabled` and would still navigate. */}
137
+ {canProceedToCheckout ? (
138
+ <Button asChild size="lg" className="mt-6 w-full rounded px-6 text-sm">
139
+ <Link href="/checkout">{t('proceedToCheckout')}</Link>
140
+ </Button>
141
+ ) : (
142
+ <>
143
+ <Button disabled size="lg" className="mt-6 w-full rounded px-6 text-sm">
144
+ {t('proceedToCheckout')}
145
+ </Button>
146
+ <p className="text-destructive mt-2 text-xs">
147
+ {unavailableItems.length > 0
148
+ ? t('unavailableItemsHint')
149
+ : reservationExpired
150
+ ? tr('expiredHint')
151
+ : null}
152
+ </p>
153
+ </>
154
+ )}
155
+
156
+ <Link
157
+ href="/products"
158
+ className="text-muted-foreground hover:text-foreground mt-3 inline-flex w-full items-center justify-center px-6 py-2 text-sm transition-colors"
159
+ >
160
+ {tc('continueShopping')}
161
+ </Link>
162
+ </Card>
163
+ </div>
164
+ </div>
165
+
166
+ {/* Cross-sell recommendations */}
167
+ {cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
168
+ <CartRecommendationSection
169
+ title={t('youMightAlsoNeed')}
170
+ items={cartRecs.recommendations}
171
+ className="mt-10"
172
+ />
173
+ )}
174
+ </div>
175
+ );
176
+ }
@@ -1,137 +1,137 @@
1
- 'use client';
2
-
3
- import { useState, useEffect, useCallback, useRef } from 'react';
4
- import { Clock } from 'lucide-react';
5
- import type { ReservationInfo } from 'brainerce';
6
- import { useTranslations } from '@/core/lib/translations';
7
- import { cn } from '@/core/lib/utils';
8
-
9
- interface ReservationCountdownProps {
10
- reservation: ReservationInfo;
11
- /**
12
- * Fired once per reservation window when the timer reaches zero, including
13
- * when the page opens on an already-expired reservation. The handler is
14
- * expected to refresh the cart and gate checkout: this component only
15
- * displays, it never decides what is still purchasable. Wire it to
16
- * `useCartPage().onReservationExpired`.
17
- */
18
- onExpire?: () => void;
19
- className?: string;
20
- }
21
-
22
- export function ReservationCountdown({
23
- reservation,
24
- onExpire,
25
- className,
26
- }: ReservationCountdownProps) {
27
- const t = useTranslations('reservation');
28
- // `null` means "not measured yet". Server-rendered HTML must not compute a
29
- // time, or it disagrees with the client on hydration; starting at 0 instead
30
- // would render the expired banner for one frame on a perfectly live
31
- // reservation, and would fire onExpire on every mount.
32
- const [remainingSeconds, setRemainingSeconds] = useState<number | null>(null);
33
-
34
- const calculateRemaining = useCallback(() => {
35
- if (!reservation.expiresAt) return 0;
36
- const expiresAtMs = new Date(reservation.expiresAt).getTime();
37
- const nowMs = Date.now();
38
- return Math.max(0, Math.floor((expiresAtMs - nowMs) / 1000));
39
- }, [reservation.expiresAt]);
40
-
41
- // Report each expiry window at most once. Keyed on `expiresAt` rather than a
42
- // boolean, so the cart refresh that expiry triggers (which remounts this
43
- // component) cannot bounce straight back into a second report.
44
- const expiresAt = reservation.expiresAt;
45
- const reportedExpiryRef = useRef<string | null>(null);
46
- const onExpireRef = useRef(onExpire);
47
- // Kept in a ref, and synced in an effect rather than during render, so a
48
- // caller passing an inline arrow does not restart the timer every render.
49
- useEffect(() => {
50
- onExpireRef.current = onExpire;
51
- }, [onExpire]);
52
-
53
- const reportExpired = useCallback(() => {
54
- if (!expiresAt) return;
55
- if (reportedExpiryRef.current === expiresAt) return;
56
- reportedExpiryRef.current = expiresAt;
57
- onExpireRef.current?.();
58
- }, [expiresAt]);
59
-
60
- useEffect(() => {
61
- const initial = calculateRemaining();
62
- setRemainingSeconds(initial);
63
- if (initial <= 0) reportExpired();
64
-
65
- const interval = setInterval(() => {
66
- const remaining = calculateRemaining();
67
- setRemainingSeconds(remaining);
68
-
69
- if (remaining <= 0) {
70
- clearInterval(interval);
71
- reportExpired();
72
- }
73
- }, 1000);
74
-
75
- return () => clearInterval(interval);
76
- }, [calculateRemaining, reportExpired]);
77
-
78
- if (!reservation.hasReservation) return null;
79
-
80
- const displaySeconds = remainingSeconds ?? 0;
81
- const minutes = Math.floor(displaySeconds / 60);
82
- const seconds = displaySeconds % 60;
83
- const isExpired = remainingSeconds !== null && remainingSeconds <= 0;
84
- const isUrgent = displaySeconds > 0 && displaySeconds < 120;
85
-
86
- const displayMessage = reservation.countdownMessage
87
- ? reservation.countdownMessage.replace(
88
- '{time}',
89
- `${minutes}:${seconds.toString().padStart(2, '0')}`
90
- )
91
- : null;
92
-
93
- return (
94
- <div
95
- className={cn(
96
- 'flex items-center gap-3 rounded-lg px-4 py-3 text-sm',
97
- isExpired
98
- ? 'bg-destructive/10 border-destructive/20 text-destructive border'
99
- : isUrgent
100
- ? 'border border-orange-200 bg-orange-50 text-orange-800 dark:border-orange-800 dark:bg-orange-950/30 dark:text-orange-300'
101
- : 'bg-primary/5 border-primary/20 text-foreground border',
102
- className
103
- )}
104
- >
105
- <Clock
106
- className={cn(
107
- 'h-5 w-5 flex-shrink-0',
108
- isExpired
109
- ? 'text-destructive'
110
- : isUrgent
111
- ? 'text-orange-600 dark:text-orange-400'
112
- : 'text-primary'
113
- )}
114
- aria-hidden="true"
115
- />
116
-
117
- <div className="flex-1">
118
- {isExpired ? (
119
- <>
120
- <p className="font-medium">{t('expired')}</p>
121
- <p className="text-xs opacity-90">{t('expiredHint')}</p>
122
- </>
123
- ) : displayMessage ? (
124
- <p>{displayMessage}</p>
125
- ) : (
126
- <p>
127
- {isUrgent ? `${t('hurry')} ` : ''}
128
- {t('reservedFor')}{' '}
129
- <span className="font-semibold tabular-nums">
130
- {minutes}:{seconds.toString().padStart(2, '0')}
131
- </span>
132
- </p>
133
- )}
134
- </div>
135
- </div>
136
- );
137
- }
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { Clock } from 'lucide-react';
5
+ import type { ReservationInfo } from 'brainerce';
6
+ import { useTranslations } from '@/core/lib/translations';
7
+ import { cn } from '@/core/lib/utils';
8
+
9
+ interface ReservationCountdownProps {
10
+ reservation: ReservationInfo;
11
+ /**
12
+ * Fired once per reservation window when the timer reaches zero, including
13
+ * when the page opens on an already-expired reservation. The handler is
14
+ * expected to refresh the cart and gate checkout: this component only
15
+ * displays, it never decides what is still purchasable. Wire it to
16
+ * `useCartPage().onReservationExpired`.
17
+ */
18
+ onExpire?: () => void;
19
+ className?: string;
20
+ }
21
+
22
+ export function ReservationCountdown({
23
+ reservation,
24
+ onExpire,
25
+ className,
26
+ }: ReservationCountdownProps) {
27
+ const t = useTranslations('reservation');
28
+ // `null` means "not measured yet". Server-rendered HTML must not compute a
29
+ // time, or it disagrees with the client on hydration; starting at 0 instead
30
+ // would render the expired banner for one frame on a perfectly live
31
+ // reservation, and would fire onExpire on every mount.
32
+ const [remainingSeconds, setRemainingSeconds] = useState<number | null>(null);
33
+
34
+ const calculateRemaining = useCallback(() => {
35
+ if (!reservation.expiresAt) return 0;
36
+ const expiresAtMs = new Date(reservation.expiresAt).getTime();
37
+ const nowMs = Date.now();
38
+ return Math.max(0, Math.floor((expiresAtMs - nowMs) / 1000));
39
+ }, [reservation.expiresAt]);
40
+
41
+ // Report each expiry window at most once. Keyed on `expiresAt` rather than a
42
+ // boolean, so the cart refresh that expiry triggers (which remounts this
43
+ // component) cannot bounce straight back into a second report.
44
+ const expiresAt = reservation.expiresAt;
45
+ const reportedExpiryRef = useRef<string | null>(null);
46
+ const onExpireRef = useRef(onExpire);
47
+ // Kept in a ref, and synced in an effect rather than during render, so a
48
+ // caller passing an inline arrow does not restart the timer every render.
49
+ useEffect(() => {
50
+ onExpireRef.current = onExpire;
51
+ }, [onExpire]);
52
+
53
+ const reportExpired = useCallback(() => {
54
+ if (!expiresAt) return;
55
+ if (reportedExpiryRef.current === expiresAt) return;
56
+ reportedExpiryRef.current = expiresAt;
57
+ onExpireRef.current?.();
58
+ }, [expiresAt]);
59
+
60
+ useEffect(() => {
61
+ const initial = calculateRemaining();
62
+ setRemainingSeconds(initial);
63
+ if (initial <= 0) reportExpired();
64
+
65
+ const interval = setInterval(() => {
66
+ const remaining = calculateRemaining();
67
+ setRemainingSeconds(remaining);
68
+
69
+ if (remaining <= 0) {
70
+ clearInterval(interval);
71
+ reportExpired();
72
+ }
73
+ }, 1000);
74
+
75
+ return () => clearInterval(interval);
76
+ }, [calculateRemaining, reportExpired]);
77
+
78
+ if (!reservation.hasReservation) return null;
79
+
80
+ const displaySeconds = remainingSeconds ?? 0;
81
+ const minutes = Math.floor(displaySeconds / 60);
82
+ const seconds = displaySeconds % 60;
83
+ const isExpired = remainingSeconds !== null && remainingSeconds <= 0;
84
+ const isUrgent = displaySeconds > 0 && displaySeconds < 120;
85
+
86
+ const displayMessage = reservation.countdownMessage
87
+ ? reservation.countdownMessage.replace(
88
+ '{time}',
89
+ `${minutes}:${seconds.toString().padStart(2, '0')}`
90
+ )
91
+ : null;
92
+
93
+ return (
94
+ <div
95
+ className={cn(
96
+ 'flex items-center gap-3 rounded-lg px-4 py-3 text-sm',
97
+ isExpired
98
+ ? 'bg-destructive/10 border-destructive/20 text-destructive border'
99
+ : isUrgent
100
+ ? 'border border-orange-200 bg-orange-50 text-orange-800 dark:border-orange-800 dark:bg-orange-950/30 dark:text-orange-300'
101
+ : 'bg-primary/5 border-primary/20 text-foreground border',
102
+ className
103
+ )}
104
+ >
105
+ <Clock
106
+ className={cn(
107
+ 'h-5 w-5 flex-shrink-0',
108
+ isExpired
109
+ ? 'text-destructive'
110
+ : isUrgent
111
+ ? 'text-orange-600 dark:text-orange-400'
112
+ : 'text-primary'
113
+ )}
114
+ aria-hidden="true"
115
+ />
116
+
117
+ <div className="flex-1">
118
+ {isExpired ? (
119
+ <>
120
+ <p className="font-medium">{t('expired')}</p>
121
+ <p className="text-xs opacity-90">{t('expiredHint')}</p>
122
+ </>
123
+ ) : displayMessage ? (
124
+ <p>{displayMessage}</p>
125
+ ) : (
126
+ <p>
127
+ {isUrgent ? `${t('hurry')} ` : ''}
128
+ {t('reservedFor')}{' '}
129
+ <span className="font-semibold tabular-nums">
130
+ {minutes}:{seconds.toString().padStart(2, '0')}
131
+ </span>
132
+ </p>
133
+ )}
134
+ </div>
135
+ </div>
136
+ );
137
+ }