address card provider
This commit is contained in:
@@ -3,21 +3,11 @@
|
|||||||
import { useCart } from "@/components/context/cartcontext";
|
import { useCart } from "@/components/context/cartcontext";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import { ShoppingBag, ChevronLeft, ChevronRight, Truck, CreditCard, Lock } from "lucide-react";
|
||||||
ShoppingBag,
|
|
||||||
ChevronLeft,
|
|
||||||
ChevronRight,
|
|
||||||
Truck,
|
|
||||||
CreditCard,
|
|
||||||
MapPin,
|
|
||||||
User,
|
|
||||||
Lock
|
|
||||||
} from "lucide-react";
|
|
||||||
import PaymentMethodsSection from "@/components/PaymentMethods";
|
import PaymentMethodsSection from "@/components/PaymentMethods";
|
||||||
import { getCartApi } from "@/public/src/services/cart/api";
|
import { getCartApi } from "@/public/src/services/cart/api";
|
||||||
import { fetchUserAddresses, type Address, addAddressApi, NewAddressData, updateAddressApi, deleteAddressApi } from "@/public/src/services/address/api";
|
import AddressCard from "@/components/AddressCard";
|
||||||
|
import { useAddressContext } from "@/components/context/Addresscontext";
|
||||||
|
|
||||||
|
|
||||||
export default function CheckoutPage() {
|
export default function CheckoutPage() {
|
||||||
const { cart } = useCart();
|
const { cart } = useCart();
|
||||||
@@ -25,32 +15,18 @@ export default function CheckoutPage() {
|
|||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [serverSummary, setServerSummary] = useState<any>(null);
|
const [serverSummary, setServerSummary] = useState<any>(null);
|
||||||
const [addresses, setAddresses] = useState<Address[]>([]);
|
const {
|
||||||
const [selectedAddressId, setSelectedAddressId] = useState<string | null>(null);
|
addresses, isAddressLoading, showNewAddressForm, setShowNewAddressForm,
|
||||||
const [showNewAddressForm, setShowNewAddressForm] = useState(false);
|
editingAddressId, newAddress, handleAddressInputChange,
|
||||||
const [isAddressLoading, setIsAddressLoading] = useState(true);
|
handleAddNewAddress, handleUpdateAddress, handleCancelForm
|
||||||
const [newAddress, setNewAddress] = useState<NewAddressData>({
|
} = useAddressContext();
|
||||||
title: 'خانه',
|
|
||||||
recipientName: '',
|
|
||||||
phone: '',
|
|
||||||
province: '',
|
|
||||||
city: '',
|
|
||||||
postalCode: '',
|
|
||||||
addressLine: '',
|
|
||||||
plaque: '',
|
|
||||||
unit: '',
|
|
||||||
isDefault: false
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeCheckout = async () => {
|
const initializeCheckout = async () => {
|
||||||
// توجه: در صفحه Cart از refreshToken استفاده کرده بودید، اگر اینجا accessToken است دقت کنید که یکسان باشند
|
|
||||||
const token = localStorage.getItem('accessToken') || localStorage.getItem('refreshToken');
|
const token = localStorage.getItem('accessToken') || localStorage.getItem('refreshToken');
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
// ۳. دریافت اطلاعات سبد خرید از سرور
|
|
||||||
try {
|
try {
|
||||||
const data = await getCartApi();
|
const data = await getCartApi();
|
||||||
if (data && data.summary) {
|
if (data && data.summary) {
|
||||||
@@ -59,47 +35,8 @@ export default function CheckoutPage() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("خطا در دریافت اطلاعات سبد خرید:", error);
|
console.error("خطا در دریافت اطلاعات سبد خرید:", error);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
initializeCheckout();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initializeCheckout = async () => {
|
|
||||||
const token = localStorage.getItem('accessToken') || localStorage.getItem('refreshToken');
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
try {
|
|
||||||
// دریافت همزمان اطلاعات سبد و آدرسها
|
|
||||||
const [cartData, addressData] = await Promise.all([
|
|
||||||
getCartApi(),
|
|
||||||
fetchUserAddresses()
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (cartData && cartData.summary) {
|
|
||||||
setServerSummary(cartData.summary);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addressData && addressData.length > 0) {
|
|
||||||
setAddresses(addressData);
|
|
||||||
// انتخاب آدرس پیشفرض یا اولین آدرس به عنوان انتخابشده
|
|
||||||
const defaultAddress = addressData.find(addr => addr.isDefault) || addressData[0];
|
|
||||||
setSelectedAddressId(defaultAddress.id);
|
|
||||||
} else {
|
|
||||||
// اگر آدرسی وجود نداشت، فرم افزودن آدرس را نمایش بده
|
|
||||||
setShowNewAddressForm(true);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("خطا در دریافت اطلاعات صفحه پرداخت:", error);
|
|
||||||
} finally {
|
|
||||||
setIsAddressLoading(false);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
setIsAddressLoading(false);
|
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
@@ -107,149 +44,11 @@ export default function CheckoutPage() {
|
|||||||
initializeCheckout();
|
initializeCheckout();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const handleAddressInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setNewAddress(prev => ({ ...prev, [name]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddNewAddress = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
// اینجا میتوانید ولیدیشنهای لازم را اضافه کنید
|
|
||||||
try {
|
|
||||||
const addedAddress = await addAddressApi(newAddress);
|
|
||||||
// آدرس جدید را به لیست اضافه کن و آن را به عنوان انتخاب شده قرار بده
|
|
||||||
setAddresses(prev => [...prev, addedAddress]);
|
|
||||||
setSelectedAddressId(addedAddress.id);
|
|
||||||
setShowNewAddressForm(false); // فرم را مخفی کن
|
|
||||||
// فرم را ریست کن
|
|
||||||
setNewAddress({
|
|
||||||
title: 'خانه', recipientName: '', phone: '', province: '', city: '',
|
|
||||||
postalCode: '', addressLine: '', plaque: '', unit: '', isDefault: false
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("خطا در ذخیره آدرس:", error);
|
|
||||||
// اینجا میتوانید به کاربر خطا را نمایش دهید
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [editingAddressId, setEditingAddressId] = useState(null);
|
|
||||||
|
|
||||||
// 2. تابع کلیک روی دکمه ویرایش
|
|
||||||
const handleEditClick = (address: any, e: any) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation(); // برای جلوگیری از انتخاب شدن radio button هنگام کلیک روی ویرایش
|
|
||||||
|
|
||||||
setEditingAddressId(address.id);
|
|
||||||
// پر کردن مقادیر فرم با دیتای آدرس انتخاب شده (فرض بر این است که استیت شما setNewAddress نام دارد)
|
|
||||||
setNewAddress({
|
|
||||||
title: address.title || "",
|
|
||||||
recipientName: address.recipientName || "",
|
|
||||||
phone: address.phone || "",
|
|
||||||
province: address.province || "",
|
|
||||||
city: address.city || "",
|
|
||||||
postalCode: address.postalCode || "",
|
|
||||||
addressLine: address.addressLine || "",
|
|
||||||
plaque: address.plaque || "",
|
|
||||||
unit: address.unit || "",
|
|
||||||
isDefault: address.isDefault || false
|
|
||||||
});
|
|
||||||
setShowNewAddressForm(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 3. تابع فراخوانی API ویرایش
|
|
||||||
const handleUpdateAddress = async () => {
|
|
||||||
// اضافه کردن این شرط برای جلوگیری از خطای تایپ و توقف اجرا در صورت null بودن ID
|
|
||||||
if (!editingAddressId) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await updateAddressApi(editingAddressId, newAddress);
|
|
||||||
if (response.success) {
|
|
||||||
// آپدیت کردن آدرس ویرایش شده در لیست آدرسها
|
|
||||||
setAddresses(prevAddresses =>
|
|
||||||
prevAddresses.map(addr => addr.id === editingAddressId ? response.data : addr)
|
|
||||||
);
|
|
||||||
|
|
||||||
// خروج از حالت فرم و ریست کردن مقادیر
|
|
||||||
setShowNewAddressForm(false);
|
|
||||||
setEditingAddressId(null);
|
|
||||||
setNewAddress({
|
|
||||||
title: "", recipientName: "", phone: "", province: "", city: "", postalCode: "", addressLine: "", plaque: "", unit: "", isDefault: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// مدیریت خطا (در صورت نیاز آلرت یا توست نمایش دهید)
|
|
||||||
console.error("خطا در بهروزرسانی آدرس:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 4. تابع انصراف یکپارچه شده
|
|
||||||
const handleCancelForm = () => {
|
|
||||||
setShowNewAddressForm(false);
|
|
||||||
setEditingAddressId(null);
|
|
||||||
setNewAddress({
|
|
||||||
title: "", recipientName: "", phone: "", province: "", city: "", postalCode: "", addressLine: "", plaque: "", unit: "", isDefault: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteAddress = async (addressId: string, e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation(); // جلوگیری از انتخاب شدن آدرس هنگام کلیک روی دکمه حذف
|
|
||||||
|
|
||||||
// گرفتن تاییدیه از کاربر قبل از حذف
|
|
||||||
const confirmDelete = window.confirm("آیا از حذف این آدرس اطمینان دارید؟");
|
|
||||||
if (!confirmDelete) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await deleteAddressApi(addressId);
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
// حذف آدرس از لیست موجود در State
|
|
||||||
setAddresses(prevAddresses =>
|
|
||||||
prevAddresses.filter(addr => addr.id !== addressId)
|
|
||||||
);
|
|
||||||
|
|
||||||
// اگر آدرسی که پاک شد همان آدرس انتخابشده بود، انتخاب را لغو کن
|
|
||||||
if (selectedAddressId === addressId) {
|
|
||||||
setSelectedAddressId(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// اگر آدرسی که پاک شد در حال ویرایش بود، فرم ویرایش را ببند
|
|
||||||
if (editingAddressId === addressId) {
|
|
||||||
setShowNewAddressForm(false);
|
|
||||||
setEditingAddressId(null);
|
|
||||||
setNewAddress({
|
|
||||||
title: "", recipientName: "", phone: "", province: "", city: "", postalCode: "", addressLine: "", plaque: "", unit: "", isDefault: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("خطا در حذف آدرس:", error);
|
|
||||||
// اینجا میتوانید یک Toast یا Alert برای نمایش خطا به کاربر اضافه کنید
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// محاسبه قیمت کل و تعداد
|
|
||||||
const parsePrice = (priceStr?: number | null | string) => {
|
const parsePrice = (priceStr?: number | null | string) => {
|
||||||
if (!priceStr) return 0;
|
if (!priceStr) return 0;
|
||||||
return Number(priceStr.toString().replace(/,/g, ''));
|
return Number(priceStr.toString().replace(/,/g, ''));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ۴. استفاده از اطلاعات سرور در صورت لاگین بودن، در غیر این صورت استفاده از Context
|
|
||||||
const totalPrice = isAuthenticated && serverSummary
|
const totalPrice = isAuthenticated && serverSummary
|
||||||
? serverSummary.totalPrice || serverSummary.total || 0
|
? serverSummary.totalPrice || serverSummary.total || 0
|
||||||
: cart.reduce((total, item) => total + (parsePrice(item.price) * item.quantity), 0);
|
: cart.reduce((total, item) => total + (parsePrice(item.price) * item.quantity), 0);
|
||||||
@@ -261,7 +60,6 @@ export default function CheckoutPage() {
|
|||||||
const shippingCost = shippingMethod === 'post' ? 45000 : 75000;
|
const shippingCost = shippingMethod === 'post' ? 45000 : 75000;
|
||||||
const finalPrice = totalPrice + shippingCost;
|
const finalPrice = totalPrice + shippingCost;
|
||||||
|
|
||||||
// حالت در حال بررسی توکن
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50/30">
|
<div className="min-h-screen flex items-center justify-center bg-gray-50/30">
|
||||||
@@ -270,7 +68,6 @@ export default function CheckoutPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// اگر کاربر لاگین نبود
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50/30 flex flex-col items-center justify-center p-6" dir="rtl">
|
<div className="min-h-screen bg-gray-50/30 flex flex-col items-center justify-center p-6" dir="rtl">
|
||||||
@@ -291,7 +88,6 @@ export default function CheckoutPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// رندر اصلی کامپوننت Checkout
|
|
||||||
return (
|
return (
|
||||||
<main className="bg-gray-50/30 min-h-screen pb-20" dir="rtl">
|
<main className="bg-gray-50/30 min-h-screen pb-20" dir="rtl">
|
||||||
<div className="container mx-auto px-4 py-8 max-w-6xl">
|
<div className="container mx-auto px-4 py-8 max-w-6xl">
|
||||||
@@ -336,8 +132,8 @@ export default function CheckoutPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8">
|
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8">
|
||||||
{/* ... (فرمهای آدرس گیرنده بدون تغییر) ... */}
|
|
||||||
<div className="flex-1 space-y-6">
|
<div className="flex-1 space-y-6">
|
||||||
<div className="flex-1 space-y-6">
|
<div className="flex-1 space-y-6">
|
||||||
{isAddressLoading ? (
|
{isAddressLoading ? (
|
||||||
@@ -348,8 +144,8 @@ export default function CheckoutPage() {
|
|||||||
<>
|
<>
|
||||||
{showNewAddressForm ? (
|
{showNewAddressForm ? (
|
||||||
<div className="bg-white rounded-[1rem] p-6 shadow-sm space-y-6 border border-gray-100">
|
<div className="bg-white rounded-[1rem] p-6 shadow-sm space-y-6 border border-gray-100">
|
||||||
<div className="flex justify-between items-center border-b pb-4">
|
<div className="flex justify-between items-center border-b border-gray-300 b-4">
|
||||||
<button className="text-lg cursor-pointer font-bold text-gray-800">
|
<button className="text-lg cursor-pointer mb-4 font-bold text-gray-800">
|
||||||
{editingAddressId ? 'ویرایش آدرس' : 'افزودن آدرس جدید'}
|
{editingAddressId ? 'ویرایش آدرس' : 'افزودن آدرس جدید'}
|
||||||
</button>
|
</button>
|
||||||
{addresses.length > 0 && (
|
{addresses.length > 0 && (
|
||||||
@@ -498,11 +294,7 @@ export default function CheckoutPage() {
|
|||||||
<h2 className="text-lg font-bold text-gray-800">انتخاب آدرس ارسال</h2>
|
<h2 className="text-lg font-bold text-gray-800">انتخاب آدرس ارسال</h2>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => setShowNewAddressForm(true)}
|
||||||
setEditingAddressId(null);
|
|
||||||
setNewAddress({ title: "", recipientName: "", phone: "", province: "", city: "", postalCode: "", addressLine: "", plaque: "", unit: "", isDefault: false });
|
|
||||||
setShowNewAddressForm(true);
|
|
||||||
}}
|
|
||||||
className="text-sm cursor-pointer text-blue-600 hover:text-blue-800 font-medium flex items-center gap-1"
|
className="text-sm cursor-pointer text-blue-600 hover:text-blue-800 font-medium flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<span>+</span> افزودن آدرس جدید
|
<span>+</span> افزودن آدرس جدید
|
||||||
@@ -516,88 +308,16 @@ export default function CheckoutPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{addresses.map((address) => (
|
{addresses.map((address) => (
|
||||||
<label
|
<AddressCard
|
||||||
key={address.id}
|
key={address.id}
|
||||||
className={`relative flex flex-col gap-4 p-3 cursor-pointer rounded-2xl border transition-all duration-300 ${selectedAddressId === address.id
|
address={address}
|
||||||
? 'border-[#ffb900] bg-[#ffb900]/[0.03] ring-1 ring-[#ffb900]/20 shadow-sm'
|
/>
|
||||||
: 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
{/* رادیو باتن */}
|
|
||||||
<div className="pt-0.5 relative flex items-center justify-center">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="selectedAddress"
|
|
||||||
value={address.id}
|
|
||||||
checked={selectedAddressId === address.id}
|
|
||||||
onChange={() => setSelectedAddressId(address.id)}
|
|
||||||
// حذف استایل پیشفرض و اعمال بوردر و رنگ سفارشی
|
|
||||||
className="peer appearance-none w-[18px] h-[18px] border-[1.5px] border-gray-300 rounded-full checked:border-[#ffb900] checked:bg-[#ffb900] cursor-pointer transition-all focus:outline-none focus:ring-2 focus:ring-[#ffb900]/20 focus:ring-offset-1 bg-white"
|
|
||||||
/>
|
|
||||||
{/* دایره سفید مرکزی که فقط در حالت انتخاب شده نمایش داده میشود */}
|
|
||||||
<div className="pointer-events-none absolute w-[10px] h-[10px] rounded-full bg-white opacity-0 peer-checked:opacity-100 transition-opacity"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex-1 space-y-3">
|
|
||||||
{/* عنوان و متن آدرس */}
|
|
||||||
<div>
|
|
||||||
{address.title && (
|
|
||||||
<span className="inline-block mb-2 bg-gray-50 text-gray-500 border border-gray-200 text-[11px] font-medium px-2.5 py-0.5 rounded-full">
|
|
||||||
{address.title}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<p className="font-medium text-gray-800 leading-relaxed text-sm md:text-base">
|
|
||||||
{address.addressLine}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* جزئیات آدرس (مینیمال شده) */}
|
|
||||||
<div className="flex flex-wrap items-center text-xs md:text-sm text-gray-500 gap-x-2 gap-y-1.5">
|
|
||||||
<span>{address.recipientName}</span>
|
|
||||||
<span className="text-gray-300 text-[10px]">●</span>
|
|
||||||
<span dir="ltr">{address.phone}</span>
|
|
||||||
<span className="text-gray-300 text-[10px]">●</span>
|
|
||||||
<span>کد پستی: {address.postalCode}</span>
|
|
||||||
<span className="text-gray-300 text-[10px]">●</span>
|
|
||||||
<span>
|
|
||||||
پلاک {address.plaque}
|
|
||||||
{address.unit ? `، واحد ${address.unit}` : ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* نوار دکمههای عملیاتی */}
|
|
||||||
<div className="flex items-center justify-end gap-2 pt-3 mt-1 border-t border-gray-100/80">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => handleEditClick(address, e)}
|
|
||||||
className="px-3 cursor-pointer py-1.5 text-xs font-medium text-gray-500 transition-colors rounded-lg hover:bg-blue-50 hover:text-blue-600 focus:outline-none"
|
|
||||||
>
|
|
||||||
ویرایش آدرس
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => handleDeleteAddress(address.id, e)}
|
|
||||||
className="px-3 cursor-pointer py-1.5 text-xs font-medium text-gray-500 transition-colors rounded-lg hover:bg-red-50 hover:text-red-600 focus:outline-none"
|
|
||||||
>
|
|
||||||
حذف
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="bg-white rounded-[1rem] p-6 shadow-sm space-y-4">
|
<div className="bg-white rounded-[1rem] p-6 shadow-sm space-y-4">
|
||||||
@@ -637,10 +357,8 @@ export default function CheckoutPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="w-full lg:w-[400px] shrink-0">
|
<div className="w-full lg:w-[400px] shrink-0">
|
||||||
<div className="bg-white rounded-[1rem] p-6 md:p-8 shadow-sm sticky top-6">
|
<div className="bg-white rounded-[1rem] p-6 md:p-8 shadow-sm sticky top-6">
|
||||||
<h2 className="text-lg md:text-xl font-bold text-[#1A2332] mb-6">خلاصه سفارش</h2>
|
<h2 className="text-lg md:text-xl font-bold text-[#1A2332] mb-6">خلاصه سفارش</h2>
|
||||||
@@ -654,13 +372,7 @@ export default function CheckoutPage() {
|
|||||||
<span className="font-bold text-[#1A2332]">{shippingCost.toLocaleString('fa-IR')} <span className="text-[10px] font-normal text-gray-500">تومان</span></span>
|
<span className="font-bold text-[#1A2332]">{shippingCost.toLocaleString('fa-IR')} <span className="text-[10px] font-normal text-gray-500">تومان</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ... بقیه قسمتها (متد ارسال و دکمه پرداخت) بدون تغییر ... */}
|
|
||||||
<div className="w-full border-t-2 border-dashed border-gray-100 my-6"></div>
|
<div className="w-full border-t-2 border-dashed border-gray-100 my-6"></div>
|
||||||
|
|
||||||
|
|
||||||
<div className="w-full border-t-2 border-dashed border-gray-100 my-6"></div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<PaymentMethodsSection />
|
<PaymentMethodsSection />
|
||||||
<span className="font-black mt-4 mb-4 justify-center items-center flex gap-4 text-[#1A2332] tracking-tight">
|
<span className="font-black mt-4 mb-4 justify-center items-center flex gap-4 text-[#1A2332] tracking-tight">
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import localFont from 'next/font/local';
|
|||||||
import { CartProvider } from "@/components/context/cartcontext";
|
import { CartProvider } from "@/components/context/cartcontext";
|
||||||
import { categoryService } from "@/public/src/services/categories/api";
|
import { categoryService } from "@/public/src/services/categories/api";
|
||||||
import { CategoryProvider } from "@/components/context/categoryprovider";
|
import { CategoryProvider } from "@/components/context/categoryprovider";
|
||||||
|
import { AddressProvider } from "@/components/context/Addresscontext";
|
||||||
|
|
||||||
|
|
||||||
const Yekanbakh = localFont({
|
const Yekanbakh = localFont({
|
||||||
|
|
||||||
src: [
|
src: [
|
||||||
{
|
{
|
||||||
path: '../public/src/font/YekanBakhFaNum-Regular.woff',
|
path: '../public/src/font/YekanBakhFaNum-Regular.woff',
|
||||||
@@ -35,7 +38,9 @@ export default async function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="fa" dir="rtl" className={Yekanbakh.variable}>
|
<html lang="fa" dir="rtl" className={Yekanbakh.variable}>
|
||||||
<body>
|
<body>
|
||||||
<CartProvider>
|
<AddressProvider>
|
||||||
|
|
||||||
|
<CartProvider>
|
||||||
<CategoryProvider>
|
<CategoryProvider>
|
||||||
|
|
||||||
<Header />
|
<Header />
|
||||||
@@ -43,6 +48,8 @@ export default async function RootLayout({
|
|||||||
</CategoryProvider>
|
</CategoryProvider>
|
||||||
|
|
||||||
</CartProvider>
|
</CartProvider>
|
||||||
|
</AddressProvider>
|
||||||
|
|
||||||
|
|
||||||
<Footer/>
|
<Footer/>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
95
components/AddressCard.tsx
Normal file
95
components/AddressCard.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { type Address } from "@/public/src/services/address/api";
|
||||||
|
// آدرس ایمپورت کانتکست را بر اساس مسیر فایل خود تنظیم کنید:
|
||||||
|
import { useAddressContext } from './context/Addresscontext';
|
||||||
|
|
||||||
|
interface AddressCardProps {
|
||||||
|
address: Address;
|
||||||
|
// فقط پراپ address باقی میماند
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddressCard({ address }: AddressCardProps) {
|
||||||
|
// دریافت وضعیت و توابع از کانتکست
|
||||||
|
const {
|
||||||
|
selectedAddressId,
|
||||||
|
setSelectedAddressId,
|
||||||
|
handleEditClick,
|
||||||
|
handleDeleteAddress
|
||||||
|
} = useAddressContext();
|
||||||
|
|
||||||
|
// محاسبه وضعیت انتخاب شدن بر اساس آیدی کارت و آیدی ذخیره شده در کانتکست
|
||||||
|
const isSelected = selectedAddressId === address.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={`relative flex flex-col gap-4 p-3 cursor-pointer rounded-2xl border transition-all duration-300 ${
|
||||||
|
isSelected
|
||||||
|
? 'border-[#ffb900] bg-[#ffb900]/[0.03] ring-1 ring-[#ffb900]/20 shadow-sm'
|
||||||
|
: 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
{/* رادیو باتن */}
|
||||||
|
<div className="pt-0.5 relative flex items-center justify-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="selectedAddress"
|
||||||
|
value={address.id}
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => setSelectedAddressId(address.id)} // استفاده از تابع کانتکست
|
||||||
|
className="peer appearance-none w-[18px] h-[18px] border-[1.5px] border-gray-300 rounded-full checked:border-[#ffb900] checked:bg-[#ffb900] cursor-pointer transition-all focus:outline-none focus:ring-2 focus:ring-[#ffb900]/20 focus:ring-offset-1 bg-white"
|
||||||
|
/>
|
||||||
|
{/* دایره سفید مرکزی */}
|
||||||
|
<div className="pointer-events-none absolute w-[10px] h-[10px] rounded-full bg-white opacity-0 peer-checked:opacity-100 transition-opacity"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-3">
|
||||||
|
{/* عنوان و متن آدرس */}
|
||||||
|
<div>
|
||||||
|
{address.title && (
|
||||||
|
<span className="inline-block mb-2 bg-gray-50 text-gray-500 border border-gray-200 text-[11px] font-medium px-2.5 py-0.5 rounded-full">
|
||||||
|
{address.title}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<p className="font-medium text-gray-800 leading-relaxed text-sm md:text-base">
|
||||||
|
{address.addressLine}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* جزئیات آدرس */}
|
||||||
|
<div className="flex flex-wrap items-center text-[13px] text-gray-500 gap-x-2 gap-y-1.5">
|
||||||
|
<span>{address.recipientName}</span>
|
||||||
|
<span className="text-gray-300 text-[10px]">●</span>
|
||||||
|
<span dir="ltr">{address.phone}</span>
|
||||||
|
<span className="text-gray-300 text-[10px]">●</span>
|
||||||
|
<span>کد پستی: {address.postalCode}</span>
|
||||||
|
<span className="text-gray-300 text-[10px]">●</span>
|
||||||
|
<span>
|
||||||
|
پلاک {address.plaque}
|
||||||
|
{address.unit ? `، واحد ${address.unit}` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* نوار دکمههای عملیاتی */}
|
||||||
|
<div className="flex items-center justify-end gap-2 pt-3 mt-1 border-t border-gray-100/80">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => handleEditClick(address, e)} // استفاده از تابع کانتکست
|
||||||
|
className="px-3 cursor-pointer py-1.5 text-xs font-medium text-gray-500 transition-colors rounded-lg hover:bg-blue-50 hover:text-blue-600 focus:outline-none"
|
||||||
|
>
|
||||||
|
ویرایش آدرس
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => handleDeleteAddress(address.id, e)} // استفاده از تابع کانتکست
|
||||||
|
className="px-3 cursor-pointer py-1.5 text-xs font-medium text-gray-500 transition-colors rounded-lg hover:bg-red-50 hover:text-red-600 focus:outline-none"
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
155
components/context/Addresscontext.tsx
Normal file
155
components/context/Addresscontext.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
|
import { fetchUserAddresses, addAddressApi, updateAddressApi, deleteAddressApi, type Address, type NewAddressData } from "@/public/src/services/address/api";
|
||||||
|
|
||||||
|
interface AddressContextType {
|
||||||
|
addresses: Address[];
|
||||||
|
selectedAddressId: string | null;
|
||||||
|
setSelectedAddressId: (id: string | null) => void;
|
||||||
|
isAddressLoading: boolean;
|
||||||
|
showNewAddressForm: boolean;
|
||||||
|
setShowNewAddressForm: (show: boolean) => void;
|
||||||
|
editingAddressId: string | null;
|
||||||
|
newAddress: NewAddressData;
|
||||||
|
handleAddressInputChange: (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => void;
|
||||||
|
handleAddNewAddress: (e: React.FormEvent) => Promise<void>;
|
||||||
|
handleEditClick: (address: Address, e: any) => void;
|
||||||
|
handleUpdateAddress: () => Promise<void>;
|
||||||
|
handleCancelForm: () => void;
|
||||||
|
handleDeleteAddress: (addressId: string, e: React.MouseEvent) => Promise<void>;
|
||||||
|
fetchAddresses: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddressContext = createContext<AddressContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const useAddressContext = () => {
|
||||||
|
const context = useContext(AddressContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useAddressContext must be used within an AddressProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddressProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [addresses, setAddresses] = useState<Address[]>([]);
|
||||||
|
const [selectedAddressId, setSelectedAddressId] = useState<string | null>(null);
|
||||||
|
const [isAddressLoading, setIsAddressLoading] = useState(true);
|
||||||
|
const [showNewAddressForm, setShowNewAddressForm] = useState(false);
|
||||||
|
const [editingAddressId, setEditingAddressId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const initialAddressState: NewAddressData = {
|
||||||
|
title: 'خانه', recipientName: '', phone: '', province: '', city: '',
|
||||||
|
postalCode: '', addressLine: '', plaque: '', unit: '', isDefault: false
|
||||||
|
};
|
||||||
|
const [newAddress, setNewAddress] = useState<NewAddressData>(initialAddressState);
|
||||||
|
|
||||||
|
const fetchAddresses = async () => {
|
||||||
|
setIsAddressLoading(true);
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('accessToken') || localStorage.getItem('refreshToken');
|
||||||
|
if (!token) {
|
||||||
|
setIsAddressLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const addressData = await fetchUserAddresses();
|
||||||
|
if (addressData && addressData.length > 0) {
|
||||||
|
setAddresses(addressData);
|
||||||
|
const defaultAddress = addressData.find(addr => addr.isDefault) || addressData[0];
|
||||||
|
setSelectedAddressId(defaultAddress.id);
|
||||||
|
} else {
|
||||||
|
setShowNewAddressForm(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("خطا در دریافت آدرسها:", error);
|
||||||
|
} finally {
|
||||||
|
setIsAddressLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAddresses();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddressInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setNewAddress(prev => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddNewAddress = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const addedAddress = await addAddressApi(newAddress);
|
||||||
|
setAddresses(prev => [...prev, addedAddress]);
|
||||||
|
setSelectedAddressId(addedAddress.id);
|
||||||
|
setShowNewAddressForm(false);
|
||||||
|
setNewAddress(initialAddressState);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("خطا در ذخیره آدرس:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = (address: any, e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setEditingAddressId(address.id);
|
||||||
|
setNewAddress({
|
||||||
|
title: address.title || "", recipientName: address.recipientName || "", phone: address.phone || "",
|
||||||
|
province: address.province || "", city: address.city || "", postalCode: address.postalCode || "",
|
||||||
|
addressLine: address.addressLine || "", plaque: address.plaque || "", unit: address.unit || "",
|
||||||
|
isDefault: address.isDefault || false
|
||||||
|
});
|
||||||
|
setShowNewAddressForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateAddress = async () => {
|
||||||
|
if (!editingAddressId) return;
|
||||||
|
try {
|
||||||
|
const response = await updateAddressApi(editingAddressId, newAddress);
|
||||||
|
if (response.success) {
|
||||||
|
setAddresses(prev => prev.map(addr => addr.id === editingAddressId ? response.data : addr));
|
||||||
|
setShowNewAddressForm(false);
|
||||||
|
setEditingAddressId(null);
|
||||||
|
setNewAddress(initialAddressState);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("خطا در بهروزرسانی آدرس:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelForm = () => {
|
||||||
|
setShowNewAddressForm(false);
|
||||||
|
setEditingAddressId(null);
|
||||||
|
setNewAddress(initialAddressState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAddress = async (addressId: string, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const confirmDelete = window.confirm("آیا از حذف این آدرس اطمینان دارید؟");
|
||||||
|
if (!confirmDelete) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await deleteAddressApi(addressId);
|
||||||
|
if (response.success) {
|
||||||
|
setAddresses(prev => prev.filter(addr => addr.id !== addressId));
|
||||||
|
if (selectedAddressId === addressId) setSelectedAddressId(null);
|
||||||
|
if (editingAddressId === addressId) {
|
||||||
|
setShowNewAddressForm(false);
|
||||||
|
setEditingAddressId(null);
|
||||||
|
setNewAddress(initialAddressState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("خطا در حذف آدرس:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
addresses, selectedAddressId, setSelectedAddressId, isAddressLoading,
|
||||||
|
showNewAddressForm, setShowNewAddressForm, editingAddressId, newAddress,
|
||||||
|
handleAddressInputChange, handleAddNewAddress, handleEditClick,
|
||||||
|
handleUpdateAddress, handleCancelForm, handleDeleteAddress, fetchAddresses
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AddressContext.Provider value={value}>{children}</AddressContext.Provider>;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user