feat: checout page implemented
This commit is contained in:
parent
adf31dc999
commit
f94ee531a3
4 changed files with 182 additions and 34 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowRight, ShoppingBasket } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { getCart } from "@/lib/api/cart";
|
import { getCart } from "@/lib/api/cart";
|
||||||
import { requireSession } from "@/lib/auth/session";
|
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";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
export default async function CartPage() {
|
export default async function CartPage() {
|
||||||
|
|
@ -41,36 +41,7 @@ export default async function CartPage() {
|
||||||
|
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
{cart.groups.map((group) => (
|
{cart.groups.map((group) => (
|
||||||
<section
|
<StoreCartCard key={group.store.uuid} group={group}>
|
||||||
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>
|
|
||||||
|
|
||||||
{!group.meets_minimum_order && (
|
{!group.meets_minimum_order && (
|
||||||
<p className="mt-3 rounded-lg bg-error/10 px-3 py-2 text-xs text-error">
|
<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
|
<Link
|
||||||
href={group.meets_minimum_order ? "/checkout" : "#"}
|
href={group.meets_minimum_order ? `/checkout/${group.store.uuid}` : "#"}
|
||||||
aria-disabled={!group.meets_minimum_order}
|
aria-disabled={!group.meets_minimum_order}
|
||||||
className="mt-4 flex justify-center"
|
className="mt-4 flex justify-center"
|
||||||
>
|
>
|
||||||
|
|
@ -90,7 +61,7 @@ export default async function CartPage() {
|
||||||
تکمیل سفارش
|
تکمیل سفارش
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</section>
|
</StoreCartCard>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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