feat: C08 page implemented

This commit is contained in:
Afra ‌ 2026-09-02 15:14:43 +03:30
parent 2363fc0f88
commit 2048e99bf2
5 changed files with 87 additions and 12 deletions

View file

@ -4,10 +4,16 @@ import { ArrowRight } from "lucide-react";
import { getCart } from "@/lib/api/cart";
import { getAddresses } from "@/lib/api/addresses";
import { requireSession } from "@/lib/auth/session";
import { CheckoutAddress } from "@/components/customer/checkout-address";
import { CheckoutPayment } from "@/components/customer/checkout-payment";
export default async function ConfirmPaymentPage({ params }: PageProps<"/checkout/[store]/confirm">) {
export default async function ConfirmPaymentPage({
params,
searchParams,
}: PageProps<"/checkout/[store]/confirm">) {
const { store } = await params;
const { fulfillment } = await searchParams;
const isDelivery = fulfillment === "delivery";
await requireSession(`/checkout/${store}/confirm`);
const [cart, addresses] = await Promise.all([
@ -19,6 +25,8 @@ export default async function ConfirmPaymentPage({ params }: PageProps<"/checkou
const group = cart.groups.find((g) => g.store.uuid === store);
if (!group) notFound();
const address = addresses.find((a) => a.is_default) ?? addresses[0] ?? null;
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 pattern as /cart and /checkout/[store]. */}
@ -29,7 +37,9 @@ export default async function ConfirmPaymentPage({ params }: PageProps<"/checkou
<h1 className="text-[18px] font-bold leading-none text-foreground">تأیید و پرداخت</h1>
</div>
<CheckoutPayment group={group} addresses={addresses} />
{isDelivery && <CheckoutAddress address={address} />}
<CheckoutPayment group={group} addresses={addresses} isDelivery={isDelivery} />
</main>
);
}

View file

@ -0,0 +1,54 @@
import Link from "next/link";
import { MapPin } from "lucide-react";
import type { Address, AddressLabel } from "@/types/api";
const ADDRESS_LABEL_FA: Record<AddressLabel, string> = {
home: "خانه",
work: "محل کار",
other: "سایر",
};
/**
* "افزودن آدرس" / "تغییر آدرس" both just go to /addresses for now — the picker step
* in between (per the design) is a later task.
*/
export function CheckoutAddress({ address }: { address: Address | null }) {
return (
<section className="flex flex-col gap-3">
<h2 className="text-[15px] font-bold leading-none text-foreground">آدرس تحویل</h2>
{address ? (
<div className="flex flex-col gap-3 rounded-2xl border border-gray-300 p-4">
{/* 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">
<Link href="/addresses" className="text-[13px] font-semibold leading-none text-primary-action">
تغییر آدرس
</Link>
<div className="flex items-center gap-1.5">
<span className="text-[14px] font-bold leading-none text-foreground">
{address.title || ADDRESS_LABEL_FA[address.label]}
</span>
<MapPin className="h-4 w-4 text-primary-action" />
</div>
</div>
<p className="text-end text-[13px] leading-normal text-gray-600">{address.full_address}</p>
</div>
) : (
<div className="flex flex-col items-center gap-3 rounded-2xl border border-gray-300 p-6 text-center">
<span className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-100">
<MapPin className="h-5 w-5 text-gray-500" />
</span>
<p className="text-[13px] leading-normal text-gray-600">
هنوز آدرسی ثبت نکرده‌اید. برای ادامه سفارش، لطفاً یک آدرس اضافه کنید.
</p>
<Link href="/addresses" className="w-full">
<button type="button" className="w-full rounded-xl bg-primary-action py-3 text-[14px] font-bold text-white">
افزودن آدرس
</button>
</Link>
</div>
)}
</section>
);
}

View file

@ -94,9 +94,12 @@ export function CheckoutFulfillment({ group }: { group: CartGroup }) {
</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)]">
{/* 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. */}
<Link href={pickup ? `/checkout/${group.store.uuid}/confirm` : "/checkout"} className="w-full">
{/* Same confirm+pay screen either way — it shows the address section only for
delivery, and folds the delivery fee into the total only for delivery. */}
<Link
href={`/checkout/${group.store.uuid}/confirm?fulfillment=${pickup ? "pickup" : "delivery"}`}
className="w-full"
>
<button
type="button"
className="w-full rounded-2xl bg-primary-action py-4 text-[15px] font-bold text-white"

View file

@ -12,14 +12,22 @@ const PAYMENT_OPTIONS: { value: PaymentMethod; label: string }[] = [
{ value: "cash_on_delivery", label: "پرداخت در محل" },
];
export function CheckoutPayment({ group, addresses }: { group: CartGroup; addresses: Address[] }) {
export function CheckoutPayment({
group,
addresses,
isDelivery,
}: {
group: CartGroup;
addresses: Address[];
isDelivery: boolean;
}) {
const router = useRouter();
const [method, setMethod] = useState<PaymentMethod>("wallet");
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
// Pickup flow — no delivery fee, matches the fulfillment screen's "دریافت حضوری" total.
const payable = group.items_subtotal;
// Pickup — no delivery fee, matches the fulfillment screen's "دریافت حضوری" total.
const payable = group.items_subtotal + (isDelivery ? group.delivery_fee : 0);
function handlePay() {
setError(null);

View file

@ -1,7 +1,7 @@
import "server-only";
import { getValidSession } from "@/lib/auth/session";
import { ApiError, type WinofyErrorDetails } from "./errors";
import { logWinofyRequest, logWinofyResponse } from "./dev-logger";
import { logApiRequest, logApiResponse } from "./dev-logger";
const BASE_URL = process.env.GOOYAL_ACCOUNTS_BASE_URL!;
@ -35,7 +35,7 @@ export async function gooyalFetch<T>(path: string, options: GooyalFetchOptions =
}
}
logWinofyRequest?.(method, url.toString(), body);
logApiRequest("gooyal", method, url.toString(), body);
const res = await fetch(url.toString(), {
...rest,
@ -45,12 +45,12 @@ export async function gooyalFetch<T>(path: string, options: GooyalFetchOptions =
});
if (res.status === 204) {
logWinofyResponse?.(method, url.toString(), res.status, null);
logApiResponse("gooyal", method, url.toString(), res.status, null);
return undefined as T;
}
const data = await res.json().catch(() => null);
logWinofyResponse?.(method, url.toString(), res.status, data);
logApiResponse("gooyal", method, url.toString(), res.status, data);
if (!res.ok) {
const envelope = data as { status_message?: string; details?: WinofyErrorDetails } | null;