feat: checout page implemented

This commit is contained in:
Afra ‌ 2026-09-02 01:55:25 +03:30
parent adf31dc999
commit f94ee531a3
4 changed files with 182 additions and 34 deletions

View file

@ -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>

View 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>
);
}

View 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>
);
}

View 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>
);
}