feat: C08 page implemented
This commit is contained in:
parent
2363fc0f88
commit
2048e99bf2
5 changed files with 87 additions and 12 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
54
src/components/customer/checkout-address.tsx
Normal file
54
src/components/customer/checkout-address.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue