list of addresses

This commit is contained in:
hamid zarghami
2025-11-27 10:48:58 +03:30
parent 010a1c65b0
commit 9c16a6143c
4 changed files with 213 additions and 51 deletions
@@ -1,5 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { createAddress, getAddresses } from "../service/AddressService";
import {
createAddress,
getAddresses,
updateAddress,
deleteAddress,
setDefaultAddress,
} from "../service/AddressService";
export const useGetAddresses = () => {
return useQuery({
@@ -17,3 +23,33 @@ export const useCreateAddress = () => {
},
});
};
export const useUpdateAddress = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateAddress,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["addresses"] });
},
});
};
export const useDeleteAddress = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteAddress,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["addresses"] });
},
});
};
export const useSetDefaultAddress = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: setDefaultAddress,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["addresses"] });
},
});
};
+133 -47
View File
@@ -2,25 +2,74 @@
import Button from '@/components/button/PrimaryButton';
import { ef } from '@/lib/helpers/utfNumbers';
// import { useProfile } from '@/hooks/auth/useProfile';
// import { useAuthStore } from '@/zustand/authStore';
import { ArrowLeft, Edit2, TickCircle } from 'iconsax-react';
import { ArrowLeft, Edit2, TickCircle, Trash } from 'iconsax-react';
import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import React from 'react'
import React, { useState } from 'react';
import { useGetAddresses, useSetDefaultAddress, useDeleteAddress } from './hooks/useAddressData';
import { Address } from './types/Types';
import Prompt from '@/components/utils/Prompt';
import { toast } from '@/components/Toast';
import useToggle from '@/hooks/helpers/useToggle';
type Props = object
function UserAddressesPage({ }: Props) {
const { data: addressesResponse, isLoading } = useGetAddresses();
const router = useRouter();
const searchParams = useSearchParams();
const redirect = searchParams.get('redirect');
const { mutate: setDefault, isPending: isSettingDefault } = useSetDefaultAddress();
const { mutate: deleteAddressMutation, isPending: isDeleting } = useDeleteAddress();
const { state: deleteModalVisible, set: setDeleteModal } = useToggle();
const [selectedAddressId, setSelectedAddressId] = useState<string | null>(null);
const onSelected = () => {
if(redirect) {
router.replace(redirect);
}
}
const addresses = addressesResponse?.data || [];
const handleSetDefault = (addressId: string) => {
setDefault(addressId, {
onSuccess: () => {
toast('آدرس به عنوان پیش‌فرض تنظیم شد', 'success');
if (redirect) {
router.replace(redirect);
}
},
onError: (error) => {
const errorMessage = error instanceof Error ? error.message : 'خطا در تنظیم آدرس پیش‌فرض';
toast(errorMessage, 'error');
},
});
};
const handleDeleteClick = (addressId: string) => {
setSelectedAddressId(addressId);
setDeleteModal(true);
};
const handleDeleteConfirm = () => {
if (!selectedAddressId) return;
deleteAddressMutation(selectedAddressId, {
onSuccess: () => {
toast('آدرس با موفقیت حذف شد', 'success');
setDeleteModal(false);
setSelectedAddressId(null);
},
onError: (error) => {
const errorMessage = error instanceof Error ? error.message : 'خطا در حذف آدرس';
toast(errorMessage, 'error');
},
});
};
const handleDeleteCancel = () => {
setDeleteModal(false);
setSelectedAddressId(null);
};
const formatAddress = (address: Address) => {
return `${address.address}، ${address.city}، ${address.province}`;
};
return (
<div className='overflow-y-auto h-full noscrollbar flex flex-col'>
@@ -36,45 +85,71 @@ function UserAddressesPage({ }: Props) {
</div>
<div className="mt-8 flex-1 w-full py-0 flex flex-col gap-4">
{/* <div className='flex-1 flex flex-col items-center justify-center gap-4'>
<Danger className='stroke-disabled-text size-9' />
<p className='text-center text-sm2 text-disabled-text'>
شما هیچ آدرسی ثبت نکرده اید.برای ثبت آدرس دکمه افزودن را بزنید
</p>
</div> */}
<div className="bg-container rounded-container w-full shadow-xl px-4 py-4 flex flex-col justify-between">
<h2 className='text-sm font-medium'>منزل</h2>
<p className='text-sm2 mt-2'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است
</p>
<p className='text-xs mt-3'>
{ef('ارسال: 10,000 تومان')}
</p>
<div className="flex justify-between items-center mt-4">
<Button
onClick={onSelected}
className='!bg-background !text-foreground flex items-center gap-2 text-xs h-8'
>
<TickCircle variant='Bold' className='fill-foreground stroke-background size-4.5 mb-0.5' />
تنظیم به عنوان فعال
</Button>
<Link href={'address/new'}>
<Button className='!bg-background !text-foreground flex justify-center items-center gap-2 text-xs p-0! size-8!'>
<Edit2 className='stroke-foreground size-5 mb-0.5' />
</Button>
</Link>
{isLoading ? (
<div className='flex-1 flex flex-col items-center justify-center gap-4'>
<p className='text-center text-sm2 text-disabled-text'>
در حال بارگذاری...
</p>
</div>
</div>
<div className="bg-container rounded-container w-full shadow-xl px-4 py-4 flex flex-col justify-between">
<h2 className='text-sm font-medium'>شرکت</h2>
<p className='text-sm2 mt-2'>
لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ، و با استفاده از طراحان گرافیک است
</p>
<p className='text-xs mt-3'>
{ef('ارسال: 21,000 تومان')}
</p>
</div>
) : addresses.length === 0 ? (
<div className='flex-1 flex flex-col items-center justify-center gap-4'>
<p className='text-center text-sm2 text-disabled-text'>
شما هیچ آدرسی ثبت نکرده اید. برای ثبت آدرس دکمه افزودن را بزنید
</p>
</div>
) : (
addresses.map((address) => (
<div
key={address.id}
className={`bg-container rounded-container w-full shadow-xl px-4 py-4 flex flex-col justify-between ${address.isDefault ? 'ring-2 ring-primary' : ''}`}
>
<div className="flex justify-between items-start mb-2">
<h2 className='text-sm font-medium'>{address.title}</h2>
{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 className="flex justify-between items-center mt-4 gap-2">
{!address.isDefault && (
<Button
onClick={() => handleSetDefault(address.id)}
disabled={isSettingDefault}
pending={isSettingDefault}
className='!bg-background !text-foreground flex items-center gap-2 text-xs h-8 flex-1'
>
<TickCircle variant='Bold' className='fill-foreground stroke-background size-4.5 mb-0.5' />
تنظیم به عنوان فعال
</Button>
)}
<div className="flex gap-2">
<Link href={`address/new?id=${address.id}`}>
<Button className='!bg-background !text-foreground flex justify-center items-center gap-2 text-xs p-0! size-8!'>
<Edit2 className='stroke-foreground size-5 mb-0.5' />
</Button>
</Link>
<Button
onClick={() => handleDeleteClick(address.id)}
disabled={isDeleting}
className='!bg-background !text-foreground flex justify-center items-center gap-2 text-xs p-0! size-8!'
>
<Trash className='stroke-foreground size-5 mb-0.5' />
</Button>
</div>
</div>
</div>
))
)}
</div>
<div className='w-full text-center mt-6'>
@@ -82,6 +157,17 @@ function UserAddressesPage({ }: Props) {
<Button>افزودن آدرس</Button>
</Link>
</div>
<Prompt
title='حذف آدرس'
description='آیا از حذف این آدرس اطمینان دارید؟'
textConfirm='بله، حذف کن'
textCancel='انصراف'
visible={deleteModalVisible}
onConfirm={handleDeleteConfirm}
onCancel={handleDeleteCancel}
onClick={handleDeleteCancel}
/>
</div>
)
}
@@ -1,8 +1,12 @@
import { api } from "@/lib/api/axiosInstance";
import { CreateAddressType } from "../types/Types";
import {
CreateAddressType,
AddressesResponse,
UpdateAddressType,
} from "../types/Types";
export const getAddresses = async () => {
const { data } = await api.get("/public/user/addresses");
export const getAddresses = async (): Promise<AddressesResponse> => {
const { data } = await api.get<AddressesResponse>("/public/user/addresses");
return data;
};
@@ -10,3 +14,19 @@ export const createAddress = async (params: CreateAddressType) => {
const { data } = await api.post("/public/user/addresses", params);
return data;
};
export const updateAddress = async (params: UpdateAddressType) => {
const { id, ...rest } = params;
const { data } = await api.put(`/public/user/addresses/${id}`, rest);
return data;
};
export const deleteAddress = async (id: string) => {
const { data } = await api.delete(`/public/user/addresses/${id}`);
return data;
};
export const setDefaultAddress = async (id: string) => {
const { data } = await api.patch(`/public/user/addresses/${id}/set-default`);
return data;
};
@@ -1,4 +1,24 @@
import type { Icon, DivIcon } from "leaflet";
import type { BaseResponse } from "@/app/[name]/(Main)/types/Types";
export interface Address {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
user: string;
title: string;
address: string;
city: string;
province: string;
postalCode: string;
latitude: number;
longitude: number;
phone: string;
isDefault: boolean;
}
export type AddressesResponse = BaseResponse<Address[]>;
export type CreateAddressType = {
title: string;