From 418a63734f683f907d00d9eec6b980ab748b7ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Wed, 2 Sep 2026 00:29:50 +0330 Subject: [PATCH 1/6] perf: product detail page modified --- .../(shop)/products/[uuid]/page.tsx | 8 +-- .../product/product-purchase-panel.tsx | 64 +++++++++++-------- src/components/ui/hero-back-button.tsx | 4 +- 3 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/app/(customer)/(shop)/products/[uuid]/page.tsx b/src/app/(customer)/(shop)/products/[uuid]/page.tsx index e2fbc5e..286a980 100644 --- a/src/app/(customer)/(shop)/products/[uuid]/page.tsx +++ b/src/app/(customer)/(shop)/products/[uuid]/page.tsx @@ -14,8 +14,8 @@ export default async function ProductDetailPage({ params }: PageProps<"/products if (!product) notFound(); return ( -
-
+
+
{product.image_url && ( // eslint-disable-next-line @next/next/no-img-element {product.name} @@ -24,9 +24,9 @@ export default async function ProductDetailPage({ params }: PageProps<"/products
-
+

{product.name}

- + {product.store.name}

diff --git a/src/components/product/product-purchase-panel.tsx b/src/components/product/product-purchase-panel.tsx index 5a8d00a..ca5a299 100644 --- a/src/components/product/product-purchase-panel.tsx +++ b/src/components/product/product-purchase-panel.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useTransition, type ReactNode } from "react"; import { useRouter, usePathname } from "next/navigation"; -import { Minus, Plus, X, CheckCircle2 } from "lucide-react"; +import { Minus, Plus, X, CheckCircle2, Check } from "lucide-react"; import { addToCart } from "@/lib/api/cart"; import { toPersianDigits } from "@/lib/format/persian-digits"; import type { ProductDetail } from "@/types/api"; @@ -41,17 +41,19 @@ export function ProductPurchasePanel({ product, children }: { product: ProductDe return ( <> + {/* DOM order intentionally mirrors the screenshot's left→right layout, not reading + order — under this app's inherited dir="rtl", the first child renders at the + right edge and the last at the left, so "what should appear on the left" must + come last in JSX (stepper group, then its own "+" trailing last so it lands + leftmost within the group). */}

+ + {product.is_out_of_stock ? "ناموجود" : "موجود"} + {!product.is_out_of_stock && } +
- - {toPersianDigits(quantity)} + {toPersianDigits(quantity)} +
- - {product.is_out_of_stock ? "ناموجود" : "موجود ●"} -
@@ -73,35 +81,35 @@ export function ProductPurchasePanel({ product, children }: { product: ProductDe {error &&

{error}

} {showToast && ( -
- +
+ محصول به سبد خرید اضافه شد - +
)} -
+
-
جمع کل {(product.price * quantity).toLocaleString("fa-IR")} تومان
+
-
+
); diff --git a/src/components/ui/hero-back-button.tsx b/src/components/ui/hero-back-button.tsx index f2e5dbd..98ab8d7 100644 --- a/src/components/ui/hero-back-button.tsx +++ b/src/components/ui/hero-back-button.tsx @@ -7,9 +7,9 @@ export function HeroBackButton({ href }: { href: string }) { - + ); } -- 2.45.3 From 64e321dc3d7ab3aa9020b9e60bdf2076bc82d634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Wed, 2 Sep 2026 00:38:22 +0330 Subject: [PATCH 2/6] perf: store and product pages' bottom bar modified --- .../product/product-purchase-panel.tsx | 2 +- src/components/store/sticky-cart-bar.tsx | 33 +++++++++---------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/components/product/product-purchase-panel.tsx b/src/components/product/product-purchase-panel.tsx index ca5a299..d7f431e 100644 --- a/src/components/product/product-purchase-panel.tsx +++ b/src/components/product/product-purchase-panel.tsx @@ -94,7 +94,7 @@ export function ProductPurchasePanel({ product, children }: { product: ProductDe
-
+
جمع کل {(product.price * quantity).toLocaleString("fa-IR")} تومان diff --git a/src/components/store/sticky-cart-bar.tsx b/src/components/store/sticky-cart-bar.tsx index c0744a8..523f79d 100644 --- a/src/components/store/sticky-cart-bar.tsx +++ b/src/components/store/sticky-cart-bar.tsx @@ -1,32 +1,31 @@ import Link from "next/link"; -import { ShoppingBag } from "lucide-react"; /** designspec.md §2.6 — only rendered when this store's cart group actually has items. */ export function StickyCartBar({ itemCount, total }: { itemCount: number; total: number }) { return ( -
+
+ {/* DOM order intentionally mirrors the screenshot's left→right layout, not reading + order — under this app's inherited dir="rtl", the first child renders at the + right edge and the last at the left. */}
- - - - - {itemCount.toLocaleString("fa-IR")} - - - مشاهده سبد - - -
+
جمع کل {total.toLocaleString("fa-IR")} تومان
+ + + مشاهده سبد + + {itemCount.toLocaleString("fa-IR")} + +
-
+
); } -- 2.45.3 From adf31dc999949b40b7d4d1865508f8f8193c2ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Wed, 2 Sep 2026 01:30:30 +0330 Subject: [PATCH 3/6] perf: cart page modified --- src/app/(customer)/(shop)/cart/page.tsx | 124 ++++++++++++++-------- src/components/customer/cart-item-row.tsx | 44 +++++--- 2 files changed, 110 insertions(+), 58 deletions(-) diff --git a/src/app/(customer)/(shop)/cart/page.tsx b/src/app/(customer)/(shop)/cart/page.tsx index 243db0c..e5e3def 100644 --- a/src/app/(customer)/(shop)/cart/page.tsx +++ b/src/app/(customer)/(shop)/cart/page.tsx @@ -1,14 +1,13 @@ import Link from "next/link"; +import { ArrowRight, ShoppingBasket } from "lucide-react"; import { getCart } from "@/lib/api/cart"; import { requireSession } from "@/lib/auth/session"; import { CartItemRow } from "@/components/customer/cart-item-row"; -import { ClearCartButton } from "@/components/customer/clear-cart-button"; import { Button } from "@/components/ui/button"; export default async function CartPage() { await requireSession("/cart"); const cart = await getCart(); - const hasBlockedGroup = cart.groups.some((g) => !g.meets_minimum_order); if (cart.groups.length === 0) { return ( @@ -21,58 +20,93 @@ export default async function CartPage() { ); } + const totalItemLines = cart.groups.reduce( + (sum, g) => sum + g.items.length, + 0, + ); + return ( -
-
-

سبد خرید

- +
+ {/* DOM order intentionally mirrors the screenshot's left→right layout, not reading + order — under this app's inherited dir="rtl", the first child renders at the + right edge and the last at the left. */} +
+ + + +

+ سبد خرید +

- {cart.groups.map((group) => ( -
- - {group.store.name} - +
+ {cart.groups.map((group) => ( +
+ + {group.items.length.toLocaleString("fa-IR")} کالا + -
- {group.items.map((item) => ( - - ))} -
+ + + + {group.store.name} + + -
-
- جمع کالاها - {group.items_subtotal.toLocaleString("fa-IR")} تومان +
+ {group.items.map((item) => ( + + ))}
-
- هزینه ارسال - {group.delivery_fee.toLocaleString("fa-IR")} تومان -
-
- مجموع - {group.total.toLocaleString("fa-IR")} تومان -
-
- {!group.meets_minimum_order && ( -

- حداقل مبلغ سفارش برای این فروشگاه رعایت نشده است. -

- )} -
- ))} +
+ + جمع فروشگاه: {group.total.toLocaleString("fa-IR")} تومان + +
-
-
- مبلغ قابل پرداخت - {cart.grand_total.toLocaleString("fa-IR")} تومان + {!group.meets_minimum_order && ( +

+ حداقل مبلغ سفارش برای این فروشگاه رعایت نشده است. +

+ )} + + + + +
+ ))} +
+ +
+
+ + جمع کل سبد ({cart.groups.length.toLocaleString("fa-IR")} فروشگاه،{" "} + {totalItemLines.toLocaleString("fa-IR")} کالا) + +

+ هر فروشگاه جداگانه پرداخت و ارسال می‌شود +

- - - + + {cart.grand_total.toLocaleString("fa-IR")} تومان +
); diff --git a/src/components/customer/cart-item-row.tsx b/src/components/customer/cart-item-row.tsx index 0a92ad9..df582a2 100644 --- a/src/components/customer/cart-item-row.tsx +++ b/src/components/customer/cart-item-row.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useTransition } from "react"; import { useRouter, usePathname } from "next/navigation"; +import { Minus, Plus, Trash2 } from "lucide-react"; import { updateCartItemQuantity, removeCartItem } from "@/lib/api/cart"; import type { CartItem } from "@/types/api"; @@ -33,41 +34,58 @@ export function CartItemRow({ item }: { item: CartItem }) { } return ( -
-
-
+
+ {/* DOM order intentionally mirrors the screenshot's left→right layout, not reading + order — under this app's inherited dir="rtl", the first child renders at the + right edge and the last at the left. */} +
+
{item.product.image_url && ( // eslint-disable-next-line @next/next/no-img-element )}
-
- {item.product.name} - {item.product.price.toLocaleString("fa-IR")} تومان +
+ {item.product.name} + + {item.product.price.toLocaleString("fa-IR")} تومان +
-
+
+ +
+
- {item.quantity.toLocaleString("fa-IR")} + + {item.quantity.toLocaleString("fa-IR")} +
- {item.line_total.toLocaleString("fa-IR")} +
+ {error &&

{error}

}
); -- 2.45.3 From f94ee531a3f5c263baa2b8ae13c1dbd87d238518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Wed, 2 Sep 2026 01:55:25 +0330 Subject: [PATCH 4/6] feat: checout page implemented --- src/app/(customer)/(shop)/cart/page.tsx | 39 +------ .../(shop)/checkout/[store]/page.tsx | 31 +++++ .../customer/checkout-fulfillment.tsx | 108 ++++++++++++++++++ src/components/customer/store-cart-card.tsx | 38 ++++++ 4 files changed, 182 insertions(+), 34 deletions(-) create mode 100644 src/app/(customer)/(shop)/checkout/[store]/page.tsx create mode 100644 src/components/customer/checkout-fulfillment.tsx create mode 100644 src/components/customer/store-cart-card.tsx diff --git a/src/app/(customer)/(shop)/cart/page.tsx b/src/app/(customer)/(shop)/cart/page.tsx index e5e3def..56ee906 100644 --- a/src/app/(customer)/(shop)/cart/page.tsx +++ b/src/app/(customer)/(shop)/cart/page.tsx @@ -1,8 +1,8 @@ import Link from "next/link"; -import { ArrowRight, ShoppingBasket } from "lucide-react"; +import { ArrowRight } from "lucide-react"; import { getCart } from "@/lib/api/cart"; import { requireSession } from "@/lib/auth/session"; -import { CartItemRow } from "@/components/customer/cart-item-row"; +import { StoreCartCard } from "@/components/customer/store-cart-card"; import { Button } from "@/components/ui/button"; export default async function CartPage() { @@ -41,36 +41,7 @@ export default async function CartPage() {
{cart.groups.map((group) => ( -
- - {group.items.length.toLocaleString("fa-IR")} کالا - - - - - - {group.store.name} - - - -
- {group.items.map((item) => ( - - ))} -
- -
- - جمع فروشگاه: {group.total.toLocaleString("fa-IR")} تومان - -
- + {!group.meets_minimum_order && (

حداقل مبلغ سفارش برای این فروشگاه رعایت نشده است. @@ -78,7 +49,7 @@ export default async function CartPage() { )} @@ -90,7 +61,7 @@ export default async function CartPage() { تکمیل سفارش -

+ ))}
diff --git a/src/app/(customer)/(shop)/checkout/[store]/page.tsx b/src/app/(customer)/(shop)/checkout/[store]/page.tsx new file mode 100644 index 0000000..b295587 --- /dev/null +++ b/src/app/(customer)/(shop)/checkout/[store]/page.tsx @@ -0,0 +1,31 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { ArrowRight } from "lucide-react"; +import { getCart } from "@/lib/api/cart"; +import { requireSession } from "@/lib/auth/session"; +import { StoreCartCard } from "@/components/customer/store-cart-card"; +import { CheckoutFulfillment } from "@/components/customer/checkout-fulfillment"; + +export default async function StoreCheckoutPage({ params }: PageProps<"/checkout/[store]">) { + const { store } = await params; + await requireSession(`/checkout/${store}`); + const cart = await getCart(); + const group = cart.groups.find((g) => g.store.uuid === store); + if (!group) notFound(); + + return ( +
+ {/* Same header as /cart, same DOM-order note: first child renders right, last left. */} +
+ + + +

سبد خرید

+
+ + + + +
+ ); +} diff --git a/src/components/customer/checkout-fulfillment.tsx b/src/components/customer/checkout-fulfillment.tsx new file mode 100644 index 0000000..7ca7b10 --- /dev/null +++ b/src/components/customer/checkout-fulfillment.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import type { CartGroup } from "@/types/api"; + +/** + * "دریافت حضوری" vs "ارسال توسط فروشگاه" has no backing field in the Checkout API — + * the OpenAPI spec's Checkout schema is address_uuid/delivery_type/scheduled_at/ + * payment_method/notes only, with delivery_type meaning express-vs-scheduled timing, + * not pickup-vs-delivery fulfillment. So this toggle only affects what's *displayed* + * here; the actual order submission (next screen) still uses the real delivery fee. + */ +export function CheckoutFulfillment({ group }: { group: CartGroup }) { + const [pickup, setPickup] = useState(false); + const deliveryFee = pickup ? 0 : group.delivery_fee; + const payable = group.items_subtotal + deliveryFee; + + return ( +
+
+

نحوه دریافت

+ + + + +
+ +
+
+ جمع کالاها + {group.items_subtotal.toLocaleString("fa-IR")} تومان +
+
+ هزینه ارسال + {deliveryFee.toLocaleString("fa-IR")} تومان +
+
+ مبلغ قابل پرداخت + + {payable.toLocaleString("fa-IR")} تومان + +
+
+ +
+ + + +
+
+ ); +} diff --git a/src/components/customer/store-cart-card.tsx b/src/components/customer/store-cart-card.tsx new file mode 100644 index 0000000..9500a32 --- /dev/null +++ b/src/components/customer/store-cart-card.tsx @@ -0,0 +1,38 @@ +import Link from "next/link"; +import { ShoppingBasket } from "lucide-react"; +import { CartItemRow } from "@/components/customer/cart-item-row"; +import type { CartGroup } from "@/types/api"; +import type { ReactNode } from "react"; + +/** Shared between /cart and /checkout/[store] — the pixel-perfect per-store card. */ +export function StoreCartCard({ group, children }: { group: CartGroup; children?: ReactNode }) { + return ( +
+ + {group.items.length.toLocaleString("fa-IR")} کالا + + + + + {group.store.name} + + +
+ {group.items.map((item) => ( + + ))} +
+ +
+ + جمع فروشگاه: {group.items_subtotal.toLocaleString("fa-IR")} تومان + +
+ + {children} +
+ ); +} -- 2.45.3 From 08e4a1a98a22b33948f285699649ac85b41fe17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Wed, 2 Sep 2026 10:40:24 +0330 Subject: [PATCH 5/6] feat: checkout page implemented --- src/components/customer/checkout-fulfillment.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/customer/checkout-fulfillment.tsx b/src/components/customer/checkout-fulfillment.tsx index 7ca7b10..a3e638a 100644 --- a/src/components/customer/checkout-fulfillment.tsx +++ b/src/components/customer/checkout-fulfillment.tsx @@ -94,7 +94,9 @@ export function CheckoutFulfillment({ group }: { group: CartGroup }) {
- + {/* Pickup has its own confirm+pay screen (no address needed); delivery still + goes to the general /checkout form until that flow's design is provided. */} + +
+
+ ); +} -- 2.45.3