step payment: manage address

This commit is contained in:
hamid zarghami
2025-12-03 15:24:39 +03:30
parent 8ab83d02b1
commit 27100ad3f3
6 changed files with 211 additions and 22 deletions
@@ -6,18 +6,66 @@ import PrimaryButton from '@/components/button/PrimaryButton';
import { ChevronLeft } from 'lucide-react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSetAddressCart } from '../../hooks/useOrderData';
import { useCheckoutStore } from '../../store/Store';
import useToggle from '@/hooks/helpers/useToggle';
import { AddressSelectionModal } from './AddressSelectionModal';
const formatAddress = (address: Address) => {
return `${address.address}، ${address.city}، ${address.province}`;
};
export const AddressSection = () => {
const { mutate: setAddressCart } = useSetAddressCart();
const { setIsSelectedAddress, selectedAddressId, setSelectedAddressId } = useCheckoutStore();
const params = useParams<{ name: string; id: string }>();
const { t } = useTranslation('parallels', { keyPrefix: 'OrderDetail' });
const { data: addressesResponse, isLoading } = useGetAddresses();
const addresses = addressesResponse?.data || [];
const addresses = useMemo(() => addressesResponse?.data || [], [addressesResponse?.data]);
const redirectUrl = `/${params.name}/order/checkout/${params.id}`;
const { state: modalVisible, toggle: toggleModal, set: setModalVisible } = useToggle(false);
// اگر آدرس پیش‌فرض وجود داشت، آن را تنظیم کن، در غیر این صورت آدرس اول غیر پیش‌فرض
useEffect(() => {
if (!isLoading && addresses.length > 0 && !selectedAddressId) {
const defaultAddress = addresses.find(addr => addr.isDefault);
const addressToSet = defaultAddress || addresses.filter(addr => !addr.isDefault)[0];
if (addressToSet) {
setAddressCart(addressToSet.id, {
onSuccess: () => {
setIsSelectedAddress(true);
setSelectedAddressId(addressToSet.id);
},
});
}
}
}, [isLoading, addresses, setAddressCart, setIsSelectedAddress, selectedAddressId, setSelectedAddressId]);
const handleSelectAddress = (addressId: string) => {
setAddressCart(addressId, {
onSuccess: () => {
setIsSelectedAddress(true);
setSelectedAddressId(addressId);
setModalVisible(false);
},
});
};
// پیدا کردن آدرس انتخاب شده یا آدرس پیش‌فرض یا اولین آدرس غیر پیش‌فرض
const selectedAddress = useMemo(() => {
if (!addresses.length) return null;
if (selectedAddressId) {
return addresses.find(addr => addr.id === selectedAddressId);
}
const defaultAddress = addresses.find(addr => addr.isDefault);
if (defaultAddress) {
return defaultAddress;
}
return addresses.filter(addr => !addr.isDefault)[0];
}, [addresses, selectedAddressId]);
// اگر در حال بارگذاری است، چیزی نمایش نده
if (isLoading) {
@@ -43,36 +91,44 @@ export const AddressSection = () => {
);
}
// فیلتر کردن آدرس‌های غیر پیش‌فرض
const nonDefaultAddresses = addresses.filter(addr => !addr.isDefault);
// اگر فقط آدرس پیش‌فرض دارد، نمایش نده
if (nonDefaultAddresses.length === 0) {
// اگر آدرس انتخاب شده وجود ندارد، نمایش نده
if (!selectedAddress) {
return null;
}
// آدرس اول غیر پیش‌فرض را نمایش بده
const firstNonDefaultAddress = nonDefaultAddresses[0];
return (
<section className="bg-container rounded-container box-shadow-normal p-4">
<div className="py-3">
<div className='w-full flex justify-between items-center gap-2'>
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionAddress.Title")}</h3>
<Link href={`/${params.name}/profile/address?redirect=${encodeURIComponent(redirectUrl)}`}>
<>
<section className="bg-container rounded-container box-shadow-normal p-4">
<div className="py-3">
<div className='w-full flex justify-between items-center gap-2'>
<h3 className="text-sm2 font-medium leading-5 inline-block">{t("SectionAddress.Title")}</h3>
<button
onClick={toggleModal}
className='text-sm2 text-primary dark:text-foreground cursor-pointer'
>
{t("SectionAddress.ButtonChangeAddress")}
<ChevronLeft className='inline-block mr-1 mb-0.5 stroke-primary dark:stroke-foreground' size='16' />
</button>
</Link>
</div>
<p className='mt-6 text-sm2'>
<div className='font-bold'>
{selectedAddress.title}
</div>
{formatAddress(selectedAddress)}
</p>
</div>
<p className='mt-6 text-sm2'>
{formatAddress(firstNonDefaultAddress)}
</p>
</div>
</section>
</section>
<AddressSelectionModal
visible={modalVisible}
addresses={addresses}
redirectUrl={redirectUrl}
name={params.name}
selectedAddressId={selectedAddressId}
onClose={() => setModalVisible(false)}
onSelectAddress={handleSelectAddress}
/>
</>
);
};
@@ -0,0 +1,95 @@
'use client';
import { Address } from '@/app/[name]/(Profile)/profile/address/types/Types';
import PrimaryButton from '@/components/button/PrimaryButton';
import Modal from '@/components/utils/Modal';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { ef } from '@/lib/helpers/utfNumbers';
const formatAddress = (address: Address) => {
return `${address.address}، ${address.city}، ${address.province}`;
};
interface AddressSelectionModalProps {
visible: boolean;
addresses: Address[];
redirectUrl: string;
name: string;
selectedAddressId: string | null;
onClose: () => void;
onSelectAddress: (addressId: string) => void;
}
export const AddressSelectionModal = ({
visible,
addresses,
redirectUrl,
name,
selectedAddressId: initialSelectedAddressId,
onClose,
onSelectAddress,
}: AddressSelectionModalProps) => {
const [selectedAddressId, setSelectedAddressId] = useState<string | null>(initialSelectedAddressId);
useEffect(() => {
if (visible) {
setSelectedAddressId(initialSelectedAddressId);
} else {
setSelectedAddressId(null);
}
}, [visible, initialSelectedAddressId]);
const handleSelectAddress = (addressId: string) => {
setSelectedAddressId(addressId);
onSelectAddress(addressId);
};
return (
<Modal
visible={visible}
onClick={onClose}
>
<div className='text-right'>
<h2 className='text-base font-medium mb-6'>انتخاب آدرس</h2>
<div className='max-h-[60vh] overflow-y-auto space-y-3'>
{addresses.map((address) => (
<div
key={address.id}
onClick={() => handleSelectAddress(address.id)}
className={`bg-background rounded-container p-4 cursor-pointer transition-all ${
selectedAddressId === address.id ? 'border border-primary' : ''
}`}
>
<div className="flex justify-between items-start mb-2">
<h3 className='text-sm font-medium'>{address.title}</h3>
{address.isDefault && (
<span className='text-xs bg-primary/10 text-primary px-2 py-1 rounded-full'>
پیشفرض
</span>
)}
</div>
<p className='text-sm2 mt-2'>
{formatAddress(address)}
</p>
<p className='text-xs mt-2 text-disabled-text'>
کد پستی: {ef(address.postalCode || 'ثبت نشده')}
</p>
<p className='text-xs mt-2 text-disabled-text'>
تلفن: {ef(address.phone)}
</p>
</div>
))}
</div>
<div className='mt-6'>
<Link href={`/${name}/profile/address/new?redirect=${encodeURIComponent(redirectUrl)}`}>
<PrimaryButton className='w-full'>
افزودن آدرس جدید
</PrimaryButton>
</Link>
</div>
</div>
</Modal>
);
};
@@ -1,5 +1,9 @@
import { useQuery } from "@tanstack/react-query";
import { getPaymentMethod, getShipmentMethod } from "../service/OrderService";
import { useMutation, useQuery } from "@tanstack/react-query";
import {
getPaymentMethod,
getShipmentMethod,
setAddressCart,
} from "../service/OrderService";
export const useGetShipmentMethod = () => {
return useQuery({
@@ -14,3 +18,9 @@ export const useGetPaymentMethod = () => {
queryFn: getPaymentMethod,
});
};
export const useSetAddressCart = () => {
return useMutation({
mutationFn: setAddressCart,
});
};
@@ -17,3 +17,8 @@ export const getPaymentMethod = async (): Promise<PaymentMethodsResponse> => {
);
return data;
};
export const setAddressCart = async (id: string) => {
const { data } = await api.patch("/public/cart/address", { addressId: id });
return data;
};
@@ -0,0 +1,13 @@
import { create } from "zustand";
import { CheckoutStoreType } from "../types/Types";
export const useCheckoutStore = create<CheckoutStoreType>((set) => ({
isSelectedAddress: false,
setIsSelectedAddress: (value) => set({ isSelectedAddress: value }),
selectedAddressId: null,
setSelectedAddressId: (value) => set({ selectedAddressId: value }),
isSelectedPayment: false,
setIsSelectedPayment: (value) => set({ isSelectedPayment: value }),
isSelectedShipment: false,
setIsSelectedShipment: (value) => set({ isSelectedShipment: value }),
}));
@@ -118,3 +118,13 @@ export interface PaymentMethodResponse {
export type PaymentMethodsResponse = BaseResponse<PaymentMethodResponse[]>;
export type CheckoutStoreType = {
isSelectedAddress: boolean;
setIsSelectedAddress: (value: boolean) => void;
selectedAddressId: string | null;
setSelectedAddressId: (value: string | null) => void;
isSelectedPayment: boolean;
setIsSelectedPayment: (value: boolean) => void;
isSelectedShipment: boolean;
setIsSelectedShipment: (value: boolean) => void;
};