Skip to content

Commit 54e67dc

Browse files
committed
Merge remote-tracking branch 'origin/dev' into release213
2 parents 3dbab7b + e714c57 commit 54e67dc

2 files changed

Lines changed: 99 additions & 27 deletions

File tree

packages/evershop/src/components/frontStore/checkout/ShippingNote.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,35 @@ import {
66
} from '@components/common/ui/Card.js';
77
import { Textarea } from '@components/common/ui/Textarea.js';
88
import { useCartState } from '@components/frontStore/cart/CartContext.js';
9-
import { useCheckoutDispatch } from '@components/frontStore/checkout/CheckoutContext.js';
9+
import {
10+
useCheckout,
11+
useCheckoutDispatch
12+
} from '@components/frontStore/checkout/CheckoutContext.js';
1013
import { _ } from '@evershop/evershop/lib/locale/translate/_';
1114
import { NotebookPen } from 'lucide-react';
12-
import React, { useEffect, useState } from 'react';
15+
import React from 'react';
1316

1417
export function ShippingNote() {
18+
const { checkoutData } = useCheckout();
1519
const { updateCheckoutData } = useCheckoutDispatch();
1620
const { data: cart } = useCartState();
17-
const [note, setNote] = useState(cart?.shippingNote ?? '');
1821

19-
// This component lives outside the checkout <Form>, so it is NOT a
20-
// react-hook-form field — using TextareaField here throws because
21-
// useFormContext() is null. The note reaches the order purely through
22-
// checkoutData: the unified checkout POST sends `checkoutData`, and the
23-
// server's checkout service reads `data.note` and persists it as the cart's
24-
// `shipping_note` field.
25-
useEffect(() => {
26-
updateCheckoutData({ note });
27-
}, [note]);
22+
// This component is not a react-hook-form field — it lives outside the
23+
// checkout <Form>, so useFormContext() is null there. The note reaches the
24+
// order purely through checkoutData: the unified checkout POST sends
25+
// `checkoutData`, and the server's checkout service reads `data.note` and
26+
// persists it as the cart's `shipping_note` field.
27+
//
28+
// The value is controlled from checkoutData (not local state) because the
29+
// checkout page renders one instance per breakpoint (form column on mobile,
30+
// summary rail on desktop) — both must edit the same note. Until the
31+
// customer types, fall back to the note persisted on the cart; when `note`
32+
// is undefined at submit the server keeps the cart's stored note, so what
33+
// is displayed and what is ordered always agree.
34+
const note = checkoutData.note ?? cart?.shippingNote ?? '';
2835

2936
return (
30-
<div className="checkout-shipping-note mb-5">
37+
<div className="checkout-shipping-note">
3138
<Card className="rounded-lg border border-border shadow-none ring-0">
3239
<CardHeader>
3340
<CardTitle>
@@ -40,7 +47,7 @@ export function ShippingNote() {
4047
<CardContent>
4148
<Textarea
4249
value={note}
43-
onChange={(e) => setNote(e.target.value)}
50+
onChange={(e) => updateCheckoutData({ note: e.target.value })}
4451
placeholder={_('Add a note to your order')}
4552
rows={3}
4653
/>

packages/evershop/src/modules/checkout/pages/frontStore/checkout/Checkout.tsx

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import Area from '@components/common/Area.js';
22
import { Form } from '@components/common/form/Form.js';
3+
import {
4+
Collapsible,
5+
CollapsibleContent,
6+
CollapsibleTrigger
7+
} from '@components/common/ui/Collapsible.js';
8+
import { Skeleton } from '@components/common/ui/Skeleton.js';
9+
import { useCartState } from '@components/frontStore/cart/CartContext.js';
310
import { CartItems } from '@components/frontStore/cart/CartItems.js';
411
import { CartSummaryItemsList } from '@components/frontStore/cart/CartSummaryItems.js';
512
import { CartTotalSummary } from '@components/frontStore/cart/CartTotalSummary.js';
@@ -11,10 +18,70 @@ import { Shipment } from '@components/frontStore/checkout/Shipment.js';
1118
import { ShippingNote } from '@components/frontStore/checkout/ShippingNote.js';
1219
import { useCustomer } from '@components/frontStore/customer/CustomerContext.js';
1320
import { _ } from '@evershop/evershop/lib/locale/translate/_';
21+
import { ChevronDown } from 'lucide-react';
1422
import React from 'react';
1523
import { useForm } from 'react-hook-form';
1624
import './Checkout.scss';
1725

26+
function OrderSummaryContent() {
27+
return (
28+
<>
29+
<CartItems>
30+
{({ items, loading, showPriceIncludingTax }) => (
31+
<CartSummaryItemsList
32+
items={items}
33+
loading={loading}
34+
showPriceIncludingTax={showPriceIncludingTax}
35+
/>
36+
)}
37+
</CartItems>
38+
<div className="mt-4">
39+
<CartTotalSummary />
40+
</div>
41+
</>
42+
);
43+
}
44+
45+
/**
46+
* Below `lg` the summary rail is hidden, and this sticky disclosure bar takes
47+
* its place above the form: collapsed it shows only the label and the grand
48+
* total; expanded it reveals the full summary (items, coupon, totals) in a
49+
* panel that scrolls internally so the bar itself never grows past the
50+
* viewport. The `-mx-4` bleeds the bar through `.page-width`'s 1rem gutter so
51+
* it spans edge to edge while stuck.
52+
*/
53+
function MobileOrderSummary() {
54+
const { data: cart, loadingStates } = useCartState();
55+
const updating = Object.values(loadingStates).some(
56+
(state) => state === true || (typeof state === 'string' && state !== null)
57+
);
58+
return (
59+
<Collapsible className="checkout-mobile-summary sticky top-0 z-20 -mx-4 mb-8 border-y border-border bg-card lg:hidden">
60+
<CollapsibleTrigger className="group flex w-full items-center justify-between gap-4 px-4 py-3 text-left">
61+
<span className="flex items-center gap-1 text-sm font-medium">
62+
{_('Order summary')}
63+
<ChevronDown
64+
className="h-4 w-4 text-muted-foreground transition-transform duration-200 group-aria-expanded:rotate-180"
65+
aria-hidden="true"
66+
/>
67+
</span>
68+
{updating ? (
69+
<Skeleton className="h-5 w-16" />
70+
) : (
71+
<span className="text-base font-semibold tabular-nums">
72+
{cart?.grandTotal?.text || ''}
73+
</span>
74+
)}
75+
</CollapsibleTrigger>
76+
<CollapsibleContent className="h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 ease-out data-starting-style:h-0 data-ending-style:h-0">
77+
<div className="max-h-[60vh] overflow-y-auto border-t border-border px-4 py-4">
78+
<OrderSummaryContent />
79+
</div>
80+
</CollapsibleContent>
81+
</Collapsible>
82+
);
83+
}
84+
1885
interface CheckoutPageProps {
1986
placeOrderApi: string;
2087
getPaymentMethodApi: string;
@@ -70,6 +137,7 @@ export default function CheckoutPage({
70137
<h1 className="mb-8 text-3xl font-semibold tracking-tight">
71138
{_('Checkout')}
72139
</h1>
140+
<MobileOrderSummary />
73141
<div className="grid grid-cols-1 gap-8 pb-16 lg:grid-cols-[1fr_400px]">
74142
<Form form={form} submitBtn={false}>
75143
<Area id="checkoutFormBefore" noOuter />
@@ -81,27 +149,24 @@ export default function CheckoutPage({
81149
<ContactInformation />
82150
<Shipment />
83151
<Payment />
152+
{/* Below `lg` the summary rail is hidden, so the note moves into
153+
the form flow, right before the place-order button. Both this
154+
and the rail instance edit the same checkoutData.note. */}
155+
{showShippingNote && (
156+
<div className="mt-6 lg:hidden">
157+
<ShippingNote />
158+
</div>
159+
)}
84160
<CheckoutButton />
85161
</div>
86162
<Area id="checkoutForm" noOuter />
87163
<Area id="checkoutFormAfter" noOuter />
88164
</Form>
89-
<div className="h-fit space-y-6 lg:sticky lg:top-8">
165+
<div className="hidden h-fit space-y-6 lg:sticky lg:top-8 lg:block">
90166
{showShippingNote && <ShippingNote />}
91167
<div className="rounded-lg border border-border bg-card p-6">
92168
<h2 className="mb-4 h4">{_('Order summary')}</h2>
93-
<CartItems>
94-
{({ items, loading, showPriceIncludingTax }) => (
95-
<CartSummaryItemsList
96-
items={items}
97-
loading={loading}
98-
showPriceIncludingTax={showPriceIncludingTax}
99-
/>
100-
)}
101-
</CartItems>
102-
<div className="mt-4">
103-
<CartTotalSummary />
104-
</div>
169+
<OrderSummaryContent />
105170
</div>
106171
</div>
107172
</div>

0 commit comments

Comments
 (0)