feature/WNPT-735 #6
4 changed files with 182 additions and 34 deletions
|
|
@ -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() {
|
|||
|
||||
<div className="flex flex-col gap-8">
|
||||
{cart.groups.map((group) => (
|
||||
<section
|
||||
key={group.store.uuid}
|
||||
className="relative rounded-2xl border border-gray-300 bg-white p-6 shadow-[0_2px_10px_-6px_rgba(0,0,0,0.039),0_14px_28px_-12px_rgba(0,0,0,0.071)]"
|
||||
>
|
||||
<span className="absolute -top-3 left-4 rounded-lg bg-blue-bg px-2.5 py-1 text-[12px] font-semibold leading-none text-primary-action">
|
||||
{group.items.length.toLocaleString("fa-IR")} کالا
|
||||
</span>
|
||||
|
||||
<Link
|
||||
href={`/stores/${group.store.uuid}`}
|
||||
className="flex items-center justify-start gap-2 pb-5 mb-3 border-b border-b-[#E4E4E7]"
|
||||
>
|
||||
<ShoppingBasket className="h-4.5 w-4.5 text-foreground" />
|
||||
<span className="text-[16px] font-bold leading-none text-foreground">
|
||||
{group.store.name}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="divide-y divide-gray-200">
|
||||
{group.items.map((item) => (
|
||||
<CartItemRow key={item.uuid} item={item} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-start border-t border-gray-200 pt-5 pb-1">
|
||||
<span className="text-[13px] font-semibold leading-none text-foreground">
|
||||
جمع فروشگاه: {group.total.toLocaleString("fa-IR")} تومان
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<StoreCartCard key={group.store.uuid} group={group}>
|
||||
{!group.meets_minimum_order && (
|
||||
<p className="mt-3 rounded-lg bg-error/10 px-3 py-2 text-xs text-error">
|
||||
حداقل مبلغ سفارش برای این فروشگاه رعایت نشده است.
|
||||
|
|
@ -78,7 +49,7 @@ export default async function CartPage() {
|
|||
)}
|
||||
|
||||
<Link
|
||||
href={group.meets_minimum_order ? "/checkout" : "#"}
|
||||
href={group.meets_minimum_order ? `/checkout/${group.store.uuid}` : "#"}
|
||||
aria-disabled={!group.meets_minimum_order}
|
||||
className="mt-4 flex justify-center"
|
||||
>
|
||||
|
|
@ -90,7 +61,7 @@ export default async function CartPage() {
|
|||
تکمیل سفارش
|
||||
</button>
|
||||
</Link>
|
||||
</section>
|
||||
</StoreCartCard>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
31
src/app/(customer)/(shop)/checkout/[store]/page.tsx
Normal file
31
src/app/(customer)/(shop)/checkout/[store]/page.tsx
Normal file
|
|
@ -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 (
|
||||
<main className="mx-auto flex w-full max-w-md flex-1 flex-col gap-6 px-4 pt-4 pb-6">
|
||||
{/* Same header as /cart, same DOM-order note: first child renders right, last left. */}
|
||||
<div className="flex items-center justify-start gap-3">
|
||||
<Link href="/cart" aria-label="بازگشت">
|
||||
<ArrowRight className="h-5 w-5 text-foreground" />
|
||||
</Link>
|
||||
<h1 className="text-[18px] font-bold leading-none text-foreground">سبد خرید</h1>
|
||||
</div>
|
||||
|
||||
<StoreCartCard group={group} />
|
||||
|
||||
<CheckoutFulfillment group={group} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
108
src/components/customer/checkout-fulfillment.tsx
Normal file
108
src/components/customer/checkout-fulfillment.tsx
Normal file
|
|
@ -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 (
|
||||
<div className="flex flex-col gap-6 pb-32">
|
||||
<section className="flex flex-col gap-3">
|
||||
<h2 className="text-[15px] font-bold leading-none text-foreground">نحوه دریافت</h2>
|
||||
|
||||
<label
|
||||
className={`flex cursor-pointer flex-col gap-1 rounded-2xl border p-4 ${pickup ? "border-primary-action" : "border-gray-300"}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="fulfillment"
|
||||
checked={pickup}
|
||||
onChange={() => setPickup(true)}
|
||||
className="sr-only"
|
||||
/>
|
||||
{/* DOM order intentionally mirrors the screenshot's left→right layout — see the
|
||||
inherited dir="rtl" note on the /cart page. */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className="text-[14px] font-bold leading-none text-foreground">دریافت حضوری</span>
|
||||
<span className="text-[12px] leading-none text-gray-500">رایگان</span>
|
||||
</div>
|
||||
<span
|
||||
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 ${pickup ? "border-primary-action" : "border-gray-300"}`}
|
||||
>
|
||||
{pickup && <span className="h-2.5 w-2.5 rounded-full bg-primary-action" />}
|
||||
</span>
|
||||
</div>
|
||||
{/* The data for delivery time is mock, it should be provided from the backend*/}
|
||||
<span className="text-[12px] leading-none text-gray-500">آماده در ۱۵ دقیقه</span>
|
||||
</label>
|
||||
|
||||
<label
|
||||
className={`flex cursor-pointer flex-col gap-1 rounded-2xl border p-4 ${!pickup ? "border-primary-action" : "border-gray-300"}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="fulfillment"
|
||||
checked={!pickup}
|
||||
onChange={() => setPickup(false)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className="text-[14px] font-bold leading-none text-foreground">ارسال توسط فروشگاه</span>
|
||||
<span className="text-[12px] leading-none text-gray-500">
|
||||
{group.delivery_fee.toLocaleString("fa-IR")} تومان
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 ${!pickup ? "border-primary-action" : "border-gray-300"}`}
|
||||
>
|
||||
{!pickup && <span className="h-2.5 w-2.5 rounded-full bg-primary-action" />}
|
||||
</span>
|
||||
</div>
|
||||
{/* The data for delivery time is mock, it should be provided from the backend*/}
|
||||
<span className="text-[12px] leading-none text-gray-500">۲۵-۳۵ دقیقه</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-3 rounded-2xl bg-[#F4F4F5] px-4 py-5">
|
||||
<div className="flex items-center justify-between text-[13px] text-gray-600">
|
||||
<span>جمع کالاها</span>
|
||||
<span>{group.items_subtotal.toLocaleString("fa-IR")} تومان</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[13px] text-gray-600">
|
||||
<span>هزینه ارسال</span>
|
||||
<span>{deliveryFee.toLocaleString("fa-IR")} تومان</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-t border-gray-300 pt-3">
|
||||
<span className="text-[14px] font-semibold leading-none text-foreground">مبلغ قابل پرداخت</span>
|
||||
<span className="text-[16px] font-bold leading-none text-foreground">
|
||||
{payable.toLocaleString("fa-IR")} تومان
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="fixed inset-x-0 bottom-0 z-10 mx-auto flex w-full max-w-md flex-col items-center bg-white px-4 pt-4 pb-6 shadow-[0_-4px_15px_0_rgba(0,0,0,0.102)]">
|
||||
<Link href="/checkout" className="w-full">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full rounded-2xl bg-primary-action py-4 text-[15px] font-bold text-white"
|
||||
>
|
||||
پرداخت و ثبت سفارش — {payable.toLocaleString("fa-IR")} تومان
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/components/customer/store-cart-card.tsx
Normal file
38
src/components/customer/store-cart-card.tsx
Normal file
|
|
@ -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 (
|
||||
<section className="relative rounded-2xl border border-gray-300 bg-white p-6 shadow-[0_2px_10px_-6px_rgba(0,0,0,0.039),0_14px_28px_-12px_rgba(0,0,0,0.071)]">
|
||||
<span className="absolute -top-3 left-4 rounded-lg bg-blue-bg px-2.5 py-1 text-[12px] font-semibold leading-none text-primary-action">
|
||||
{group.items.length.toLocaleString("fa-IR")} کالا
|
||||
</span>
|
||||
|
||||
<Link
|
||||
href={`/stores/${group.store.uuid}`}
|
||||
className="mb-3 flex items-center justify-start gap-2 border-b border-b-[#E4E4E7] pb-5"
|
||||
>
|
||||
<ShoppingBasket className="h-4.5 w-4.5 text-foreground" />
|
||||
<span className="text-[16px] font-bold leading-none text-foreground">{group.store.name}</span>
|
||||
</Link>
|
||||
|
||||
<div className="divide-y divide-gray-200">
|
||||
{group.items.map((item) => (
|
||||
<CartItemRow key={item.uuid} item={item} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-start border-t border-gray-200 pt-5 pb-1">
|
||||
<span className="text-[13px] font-semibold leading-none text-foreground">
|
||||
جمع فروشگاه: {group.items_subtotal.toLocaleString("fa-IR")} تومان
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue