240 lines
9.2 KiB
TypeScript
240 lines
9.2 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState, useTransition } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { createAddress, type AddressInput } from "@/lib/api/addresses";
|
||
import type { AddressLabel, City, Neighborhood } from "@/types/api";
|
||
|
||
const LABEL_PRESETS: { value: AddressLabel; text: string }[] = [
|
||
{ value: "other", text: "سایر" },
|
||
{ value: "work", text: "محل کار" },
|
||
{ value: "home", text: "خانه جدید" },
|
||
];
|
||
|
||
export function AddressDetailsForm({
|
||
fullAddress,
|
||
latitude,
|
||
longitude,
|
||
cityUuid,
|
||
neighborhoodUuid,
|
||
cities,
|
||
neighborhoods,
|
||
next,
|
||
}: {
|
||
fullAddress: string;
|
||
latitude: number;
|
||
longitude: number;
|
||
/** Inferred from the customer's current location cookie, if they'd set one manually. */
|
||
cityUuid: string | null;
|
||
neighborhoodUuid: string | null;
|
||
cities: City[];
|
||
neighborhoods: Neighborhood[];
|
||
/** Set when this add-address detour started from checkout — see handleSubmit. */
|
||
next: string | null;
|
||
}) {
|
||
const router = useRouter();
|
||
const [address, setAddress] = useState(fullAddress);
|
||
const [label, setLabel] = useState<AddressLabel>("home");
|
||
const [title, setTitle] = useState("خانه جدید");
|
||
const [plaque, setPlaque] = useState("");
|
||
const [floor, setFloor] = useState("");
|
||
const [unit, setUnit] = useState("");
|
||
const [recipientName, setRecipientName] = useState("");
|
||
const [recipientPhone, setRecipientPhone] = useState("");
|
||
const [manualCityUuid, setManualCityUuid] = useState(cityUuid ?? "");
|
||
const [manualNeighborhoodUuid, setManualNeighborhoodUuid] = useState(neighborhoodUuid ?? "");
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [isPending, startTransition] = useTransition();
|
||
|
||
// city_uuid is a hard requirement on the backend (OpenAPI: Address.city_uuid is
|
||
// required for writes) but isn't part of this screen's design — it's normally
|
||
// inferred from the customer's current location cookie. Only show a picker for
|
||
// it when that inference failed (e.g. they're in "geo" mode, or never set one).
|
||
const needsCityPicker = !cityUuid;
|
||
const filteredNeighborhoods = useMemo(
|
||
() => neighborhoods.filter((n) => n.city.uuid === manualCityUuid),
|
||
[neighborhoods, manualCityUuid],
|
||
);
|
||
|
||
function handleSubmit(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
setError(null);
|
||
|
||
const finalCityUuid = cityUuid ?? manualCityUuid;
|
||
if (!finalCityUuid) {
|
||
setError("شهر مشخص نیست — لطفاً محله خود را از صفحه اصلی انتخاب کنید یا شهر را زیر مشخص کنید.");
|
||
return;
|
||
}
|
||
|
||
startTransition(async () => {
|
||
const input: AddressInput = {
|
||
label,
|
||
title,
|
||
full_address: address,
|
||
plaque,
|
||
floor,
|
||
unit,
|
||
recipient_name: recipientName,
|
||
recipient_phone: recipientPhone,
|
||
city_uuid: finalCityUuid,
|
||
neighborhood_uuid: (neighborhoodUuid ?? manualNeighborhoodUuid) || undefined,
|
||
latitude,
|
||
longitude,
|
||
// Arriving here from checkout means they had no address at all yet — make
|
||
// this one the default immediately so it's actually used for the order,
|
||
// rather than sending them on to /addresses to select it a second time.
|
||
// A plain profile-driven "add another address" leaves the existing default alone.
|
||
is_default: next !== null,
|
||
};
|
||
const result = await createAddress(input);
|
||
if (result.ok) {
|
||
router.push(next ?? "/addresses");
|
||
} else {
|
||
setError(result.message);
|
||
}
|
||
});
|
||
}
|
||
|
||
function selectPreset(preset: (typeof LABEL_PRESETS)[number]) {
|
||
setLabel(preset.value);
|
||
setTitle(preset.text);
|
||
}
|
||
|
||
return (
|
||
<form onSubmit={handleSubmit} className="flex flex-1 flex-col gap-6 px-4 pb-6">
|
||
<div className="flex flex-col gap-2">
|
||
<span className="text-[13px] font-semibold leading-none text-gray-700">آدرس کامل</span>
|
||
<input
|
||
type="text"
|
||
value={address}
|
||
onChange={(e) => setAddress(e.target.value)}
|
||
required
|
||
className="h-12 rounded-xl border border-gray-300 bg-white px-4 text-[14px] text-foreground outline-none focus:border-primary-action"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-2">
|
||
<span className="text-[13px] font-semibold leading-none text-gray-700">عنوان آدرس</span>
|
||
<div className="flex items-center justify-start gap-2">
|
||
{LABEL_PRESETS.map((preset) => (
|
||
<button
|
||
key={preset.value}
|
||
type="button"
|
||
onClick={() => selectPreset(preset)}
|
||
className={`rounded-full border px-4 py-2 text-[13px] font-semibold whitespace-nowrap ${
|
||
label === preset.value
|
||
? "border-primary-action bg-blue-bg text-primary-action"
|
||
: "border-gray-300 bg-white text-gray-700"
|
||
}`}
|
||
>
|
||
{preset.text}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-3 gap-3">
|
||
<div className="flex flex-col gap-2">
|
||
<span className="text-[13px] font-semibold leading-none text-gray-700">پلاک</span>
|
||
<input
|
||
type="text"
|
||
value={plaque}
|
||
onChange={(e) => setPlaque(e.target.value)}
|
||
className="h-12 rounded-xl border border-gray-300 bg-white px-3 text-center text-[14px] text-foreground outline-none focus:border-primary-action"
|
||
/>
|
||
</div>
|
||
<div className="flex flex-col gap-2">
|
||
<span className="text-[13px] font-semibold leading-none text-gray-700">طبقه</span>
|
||
<input
|
||
type="text"
|
||
value={floor}
|
||
onChange={(e) => setFloor(e.target.value)}
|
||
className="h-12 rounded-xl border border-gray-300 bg-white px-3 text-center text-[14px] text-foreground outline-none focus:border-primary-action"
|
||
/>
|
||
</div>
|
||
<div className="flex flex-col gap-2">
|
||
<span className="text-[13px] font-semibold leading-none text-gray-700">واحد</span>
|
||
<input
|
||
type="text"
|
||
value={unit}
|
||
onChange={(e) => setUnit(e.target.value)}
|
||
className="h-12 rounded-xl border border-gray-300 bg-white px-3 text-center text-[14px] text-foreground outline-none focus:border-primary-action"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-2">
|
||
<span className="text-[13px] font-semibold leading-none text-gray-700">نام گیرنده</span>
|
||
<input
|
||
type="text"
|
||
value={recipientName}
|
||
onChange={(e) => setRecipientName(e.target.value)}
|
||
required
|
||
className="h-12 rounded-xl border border-gray-300 bg-white px-4 text-[14px] text-foreground outline-none focus:border-primary-action"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-2">
|
||
<span className="text-[13px] font-semibold leading-none text-gray-700">شماره تماس</span>
|
||
<input
|
||
type="tel"
|
||
dir="ltr"
|
||
value={recipientPhone}
|
||
onChange={(e) => setRecipientPhone(e.target.value)}
|
||
placeholder="09123456789"
|
||
required
|
||
className="h-12 rounded-xl border border-gray-300 bg-white px-4 text-[14px] text-foreground outline-none placeholder:text-gray-400 focus:border-primary-action"
|
||
/>
|
||
</div>
|
||
|
||
{needsCityPicker && (
|
||
<div className="flex flex-col gap-3 rounded-xl border border-gray-300 bg-gray-50 p-4">
|
||
<p className="text-[12px] leading-normal text-gray-700">
|
||
شهر شما مشخص نیست — این فقط لازم است چون هنوز محلهای از صفحه اصلی انتخاب نکردهاید.
|
||
</p>
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<select
|
||
value={manualCityUuid}
|
||
onChange={(e) => {
|
||
setManualCityUuid(e.target.value);
|
||
setManualNeighborhoodUuid("");
|
||
}}
|
||
className="h-11 rounded-xl border border-gray-300 bg-white px-3 text-[13px]"
|
||
required
|
||
>
|
||
<option value="">شهر</option>
|
||
{cities.map((city) => (
|
||
<option key={city.uuid} value={city.uuid}>
|
||
{city.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<select
|
||
value={manualNeighborhoodUuid}
|
||
onChange={(e) => setManualNeighborhoodUuid(e.target.value)}
|
||
className="h-11 rounded-xl border border-gray-300 bg-white px-3 text-[13px]"
|
||
disabled={!manualCityUuid}
|
||
>
|
||
<option value="">محله (اختیاری)</option>
|
||
{filteredNeighborhoods.map((n) => (
|
||
<option key={n.uuid} value={n.uuid}>
|
||
{n.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{error && <p className="text-[13px] text-error">{error}</p>}
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isPending}
|
||
className="mt-auto w-full rounded-2xl bg-primary-action py-4 text-[15px] font-bold text-white disabled:opacity-40"
|
||
>
|
||
{isPending ? "در حال ذخیره..." : "ذخیره آدرس"}
|
||
</button>
|
||
</form>
|
||
);
|
||
}
|