- Fix seed-products.ts TS error (implicit any) and BOM handling - Re-run full seed to restore correct Persian encoding in DB - Fix productService query→search param mismatch - Fix IngredientWiki hardcoded port 4000→use settingsStore - Restore seed-products-data.json from git after accidental corruption
666 lines
37 KiB
TypeScript
666 lines
37 KiB
TypeScript
"use client";
|
||
import React, { useState } from "react";
|
||
import { motion, AnimatePresence } from "motion/react";
|
||
import { User, ShoppingBag, Wallet, MapPin, LogOut, ChevronRight, Package, Calendar, UserCircle, ShoppingCart, Trash2, Edit2, Phone, Hash, CheckCircle2, ArrowUpCircle, ArrowDownCircle, Info, Clock, CheckCircle, Heart, Sparkles } from "lucide-react";
|
||
import { OrderRowSkeleton } from "./Skeleton";
|
||
import { useUserStore, Address } from "../lib/store/userStore";
|
||
import { useCartStore } from "../lib/store/cartStore";
|
||
import { toPersian, cn } from "../lib/utils";
|
||
import { toast } from "sonner";
|
||
import OrderDetailsModal from "./OrderDetailsModal";
|
||
import AddressModal from "./AddressModal";
|
||
import DeleteConfirmModal from "./DeleteConfirmModal";
|
||
import TopUpModal from "./TopUpModal";
|
||
|
||
import { useRouter } from 'next/navigation';
|
||
|
||
export default function UserDashboard() {
|
||
const router = useRouter();
|
||
const { profile, logout, addAddress, updateAddress, deleteAddress, setDefaultAddress, topUpWallet } = useUserStore();
|
||
const { orders } = useCartStore();
|
||
const [activeTab, setActiveTab] = React.useState<"profile" | "orders" | "wallet" | "addresses">("profile");
|
||
const [isLoadingOrders, setIsLoadingOrders] = useState(false);
|
||
|
||
// Fetch fresh profile data (which includes orders) whenever orders tab is opened
|
||
React.useEffect(() => {
|
||
if (activeTab === "orders") {
|
||
setIsLoadingOrders(true);
|
||
useUserStore.getState().fetchProfile().finally(() => setIsLoadingOrders(false));
|
||
}
|
||
}, [activeTab]);
|
||
const [selectedOrder, setSelectedOrder] = useState<any>(null);
|
||
|
||
// Address State
|
||
const [isAddressModalOpen, setIsAddressModalOpen] = useState(false);
|
||
const [editingAddress, setEditingAddress] = useState<Address | null>(null);
|
||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||
const [addressToDelete, setAddressToDelete] = useState<Address | null>(null);
|
||
|
||
// Wallet State
|
||
const [isTopUpModalOpen, setIsTopUpModalOpen] = useState(false);
|
||
|
||
// Profile State
|
||
const [isEditing, setIsEditing] = useState(false);
|
||
const [isSavingProfile, setIsSavingProfile] = useState(false);
|
||
const [profileForm, setProfileForm] = useState({
|
||
firstName: profile.firstName || "",
|
||
lastName: profile.lastName || "",
|
||
email: profile.email || "",
|
||
mobile: profile.mobile || ""
|
||
});
|
||
|
||
// Sync profile data on change
|
||
React.useEffect(() => {
|
||
setProfileForm({
|
||
firstName: profile.firstName || "",
|
||
lastName: profile.lastName || "",
|
||
email: profile.email || "",
|
||
mobile: profile.mobile || ""
|
||
});
|
||
}, [profile]);
|
||
|
||
const handleSaveProfile = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!isEditing) return;
|
||
|
||
const cleanPhone = profileForm.mobile.trim();
|
||
if (!/^09\d{9}$/.test(cleanPhone)) {
|
||
toast.error("شماره موبایل وارد شده معتبر نیست (باید ۱۱ رقم باشد و با ۰۹ شروع شود)");
|
||
return;
|
||
}
|
||
|
||
setIsSavingProfile(true);
|
||
try {
|
||
await useUserStore.getState().updateProfile({
|
||
firstName: profileForm.firstName,
|
||
lastName: profileForm.lastName,
|
||
email: profileForm.email,
|
||
mobile: cleanPhone
|
||
});
|
||
setIsEditing(false);
|
||
toast.success("اطلاعات کاربری با موفقیت ویرایش شد");
|
||
} catch (err: any) {
|
||
toast.error(err.message || "خطا در ویرایش اطلاعات");
|
||
} finally {
|
||
setIsSavingProfile(false);
|
||
}
|
||
};
|
||
|
||
const handleLogout = () => {
|
||
logout();
|
||
router.push('/');
|
||
};
|
||
|
||
const handleSaveAddress = async (addr: Address) => {
|
||
try {
|
||
if (editingAddress) {
|
||
await updateAddress(addr.id, addr);
|
||
} else {
|
||
await addAddress(addr);
|
||
}
|
||
} catch (err) {
|
||
toast.error("خطا در ثبت آدرس");
|
||
}
|
||
setEditingAddress(null);
|
||
};
|
||
|
||
const handleDeleteClick = (addr: Address) => {
|
||
setAddressToDelete(addr);
|
||
setIsDeleteModalOpen(true);
|
||
};
|
||
|
||
const handleConfirmDelete = async () => {
|
||
if (addressToDelete) {
|
||
try {
|
||
await deleteAddress(addressToDelete.id);
|
||
toast.success("آدرس با موفقیت حذف شد");
|
||
} catch (err) {
|
||
toast.error("خطا در حذف آدرس");
|
||
}
|
||
setAddressToDelete(null);
|
||
}
|
||
};
|
||
|
||
const handleEditClick = (addr: Address) => {
|
||
setEditingAddress(addr);
|
||
setIsAddressModalOpen(true);
|
||
};
|
||
|
||
const handleAddClick = () => {
|
||
setEditingAddress(null);
|
||
setIsAddressModalOpen(true);
|
||
};
|
||
|
||
const tabs = [
|
||
{ id: "profile", label: "اطلاعات فردی", icon: <UserCircle className="w-5 h-5" /> },
|
||
{ id: "orders", label: "سفارشات من", icon: <Package className="w-5 h-5" /> },
|
||
{ id: "wallet", label: "کیف پول", icon: <Wallet className="w-5 h-5" /> },
|
||
{ id: "addresses", label: "آدرسها", icon: <MapPin className="w-5 h-5" /> },
|
||
];
|
||
|
||
return (
|
||
<div className="min-h-screen bg-medical-gray-50 pt-12 pb-24 px-4 font-vazir" dir="rtl">
|
||
<div className="max-w-6xl mx-auto">
|
||
{/* Breadcrumb */}
|
||
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-10">
|
||
<span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/')}>خانه</span>
|
||
<ChevronRight className="w-2.5 h-2.5" />
|
||
<span className="text-canina-blue">پنل کاربری</span>
|
||
</div>
|
||
|
||
<div className="grid lg:grid-cols-12 gap-8">
|
||
{/* Sidebar */}
|
||
<div className="lg:col-span-4 space-y-6">
|
||
<div className="bg-white rounded-[3rem] border border-medical-gray-200 p-8 shadow-xl">
|
||
<div className="flex items-center gap-4 mb-10">
|
||
<div className="w-16 h-16 bg-canina-blue rounded-3xl flex items-center justify-center text-white shadow-lg">
|
||
<User className="w-8 h-8" />
|
||
</div>
|
||
<div>
|
||
<h2 className="text-xl font-black text-medical-gray-900">{profile.firstName} {profile.lastName}</h2>
|
||
<p className="text-sm font-bold text-medical-gray-400">{profile.email}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
{tabs.map(tab => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id as any)}
|
||
className={`w-full flex items-center gap-3 p-4 rounded-2xl transition-all font-bold ${activeTab === tab.id ? "bg-canina-blue text-white shadow-lg shadow-canina-blue/20" : "text-medical-gray-600 hover:bg-medical-gray-50"}`}
|
||
>
|
||
{tab.icon}
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
<button
|
||
onClick={handleLogout}
|
||
className="w-full flex items-center gap-3 p-4 rounded-2xl text-red-500 hover:bg-red-50 transition-all font-bold mt-4"
|
||
>
|
||
<LogOut className="w-5 h-5" />
|
||
خروج از حساب
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-medical-gray-900 rounded-[3rem] p-8 text-white relative overflow-hidden shadow-2xl">
|
||
<div className="absolute top-0 left-0 p-6 opacity-20">
|
||
<Wallet className="w-12 h-12 text-canina-blue" />
|
||
</div>
|
||
<div className="relative z-10">
|
||
<div className="text-[10px] font-black uppercase tracking-widest text-white mb-2">موجودی کیف پول</div>
|
||
<div className="flex items-baseline gap-2 flex-row-reverse justify-end">
|
||
<div className="text-4xl font-black italic">{toPersian(profile.walletBalance.toLocaleString())}</div>
|
||
<span className="text-sm not-italic font-bold text-white/80">تومان</span>
|
||
</div>
|
||
<button
|
||
onClick={() => { setActiveTab("wallet"); setIsTopUpModalOpen(true); }}
|
||
disabled={isTopUpModalOpen}
|
||
className="mt-8 bg-canina-blue w-full py-4 rounded-2xl font-black hover:bg-white hover:text-canina-blue transition-all shadow-lg shadow-black/20 flex items-center justify-center gap-2"
|
||
>
|
||
شارژ کیف پول
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-pink-500 rounded-[3rem] p-8 text-white relative overflow-hidden shadow-2xl shadow-pink-200">
|
||
<div className="absolute -top-4 -right-4 opacity-10">
|
||
<Heart className="w-40 h-40 fill-white" />
|
||
</div>
|
||
<div className="relative z-10">
|
||
<div className="flex items-center gap-3 mb-6">
|
||
<Heart className="w-5 h-5 fill-white" />
|
||
<h4 className="text-sm font-black italic tracking-widest">ردپای مهربانی</h4>
|
||
</div>
|
||
<div className="space-y-6">
|
||
<div>
|
||
<p className="text-[10px] font-bold text-white/70 mb-1">مجموع کمکهای اهدایی</p>
|
||
<div className="flex items-baseline gap-2 flex-row-reverse justify-end">
|
||
<span className="text-3xl font-black italic">{toPersian(profile.charityDonationTotal?.toLocaleString() || "0")}</span>
|
||
<span className="text-xs font-bold opacity-60">تومان</span>
|
||
</div>
|
||
</div>
|
||
<div className="p-4 bg-white/10 rounded-2xl border border-white/20 backdrop-blur-sm">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-8 h-8 bg-white/20 rounded-xl flex items-center justify-center">
|
||
<Sparkles className="w-4 h-4 text-white" />
|
||
</div>
|
||
<p className="text-[10px] font-black leading-tight">
|
||
همکاری شما باعث تامین هزینه <span className="text-sm text-yellow-300 mx-1">{toPersian(Math.floor((profile.charityDonationTotal || 0) / 15000).toString())} وعده غذا</span> برای حیوانات بیپناه شده است.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Content */}
|
||
<div className="lg:col-span-8">
|
||
<motion.div
|
||
key={activeTab}
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
className="bg-white rounded-[3.5rem] border border-medical-gray-200 p-10 min-h-[600px] shadow-sm overflow-hidden"
|
||
>
|
||
{activeTab === "profile" && (
|
||
<div>
|
||
<h3 className="text-2xl font-black text-medical-gray-900 mb-8 italic">اطلاعات فردی</h3>
|
||
<form onSubmit={handleSaveProfile} className="space-y-8">
|
||
<div className="grid md:grid-cols-2 gap-8">
|
||
<div className="space-y-2">
|
||
<label htmlFor="firstName" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-4">نام</label>
|
||
<input
|
||
autoFocus={isEditing}
|
||
type="text"
|
||
id="firstName"
|
||
name="firstName"
|
||
autoComplete="given-name"
|
||
required
|
||
readOnly={!isEditing}
|
||
value={isEditing ? profileForm.firstName : profile.firstName}
|
||
onChange={e => setProfileForm({ ...profileForm, firstName: e.target.value })}
|
||
className={cn(
|
||
"w-full border p-4 rounded-2xl font-bold outline-none transition-all",
|
||
isEditing
|
||
? "bg-white border-canina-blue/30 focus:ring-2 focus:ring-canina-blue/20"
|
||
: "bg-medical-gray-50 border-medical-gray-100"
|
||
)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label htmlFor="lastName" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-4">نام خانوادگی</label>
|
||
<input
|
||
type="text"
|
||
id="lastName"
|
||
name="lastName"
|
||
autoComplete="family-name"
|
||
required
|
||
readOnly={!isEditing}
|
||
value={isEditing ? profileForm.lastName : profile.lastName}
|
||
onChange={e => setProfileForm({ ...profileForm, lastName: e.target.value })}
|
||
className={cn(
|
||
"w-full border p-4 rounded-2xl font-bold outline-none transition-all",
|
||
isEditing
|
||
? "bg-white border-canina-blue/30 focus:ring-2 focus:ring-canina-blue/20"
|
||
: "bg-medical-gray-50 border-medical-gray-100"
|
||
)}
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label htmlFor="email" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-4">ایمیل</label>
|
||
<input
|
||
type="email"
|
||
id="email"
|
||
name="email"
|
||
autoComplete="email"
|
||
required
|
||
readOnly={!isEditing}
|
||
value={isEditing ? profileForm.email : profile.email}
|
||
onChange={e => setProfileForm({ ...profileForm, email: e.target.value })}
|
||
className={cn(
|
||
"w-full border p-4 rounded-2xl font-bold outline-none transition-all text-left",
|
||
isEditing
|
||
? "bg-white border-canina-blue/30 focus:ring-2 focus:ring-canina-blue/20"
|
||
: "bg-medical-gray-50 border-medical-gray-100"
|
||
)}
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<label htmlFor="mobile" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-4">شماره موبایل</label>
|
||
<input
|
||
type="tel"
|
||
id="mobile"
|
||
name="mobile"
|
||
autoComplete="tel"
|
||
required
|
||
readOnly={!isEditing}
|
||
disabled={!isEditing}
|
||
value={isEditing ? profileForm.mobile : toPersian(profile.mobile || "")}
|
||
onChange={e => setProfileForm({ ...profileForm, mobile: e.target.value.replace(/[^0-9]/g, '') })}
|
||
className={cn(
|
||
"w-full border p-4 rounded-2xl font-bold outline-none transition-all text-left",
|
||
isEditing
|
||
? "bg-white border-canina-blue/30 focus:ring-2 focus:ring-canina-blue/20"
|
||
: "bg-medical-gray-50 border-medical-gray-100 opacity-75 cursor-not-allowed"
|
||
)}
|
||
dir="ltr"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex gap-4 mt-8">
|
||
{isEditing ? (
|
||
<>
|
||
<button
|
||
type="submit"
|
||
disabled={isSavingProfile}
|
||
className="bg-canina-blue text-white px-10 py-4 rounded-2xl font-black hover:bg-indigo-700 transition-all flex items-center gap-2 disabled:opacity-50"
|
||
>
|
||
{isSavingProfile && <span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />}
|
||
ذخیره تغییرات
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setProfileForm({
|
||
firstName: profile.firstName || "",
|
||
lastName: profile.lastName || "",
|
||
email: profile.email || "",
|
||
mobile: profile.mobile || ""
|
||
});
|
||
setIsEditing(false);
|
||
}}
|
||
className="bg-medical-gray-50 text-medical-gray-500 border border-medical-gray-200 px-10 py-4 rounded-2xl font-black hover:bg-medical-gray-100 transition-all"
|
||
>
|
||
انصراف
|
||
</button>
|
||
</>
|
||
) : (
|
||
<button
|
||
type="button"
|
||
onClick={() => setIsEditing(true)}
|
||
className="bg-medical-gray-900 text-white px-10 py-4 rounded-2xl font-black hover:bg-canina-blue transition-all"
|
||
>
|
||
ویرایش اطلاعات
|
||
</button>
|
||
)}
|
||
</div>
|
||
</form>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "orders" && (
|
||
<div>
|
||
<div className="flex items-center justify-between mb-8">
|
||
<h3 className="text-2xl font-black text-medical-gray-900 italic">تاریخچه سفارشات</h3>
|
||
<div className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest border border-medical-gray-100 px-4 py-2 rounded-full bg-medical-gray-50">
|
||
تعداد کل: {toPersian((orders || []).length.toString())} مورد
|
||
</div>
|
||
</div>
|
||
{(orders || []).length === 0 ? (
|
||
<div className="text-center py-20">
|
||
<ShoppingBag className="w-16 h-16 text-medical-gray-200 mx-auto mb-4" />
|
||
<p className="text-medical-gray-400 font-bold">هنوز سفارشی ثبت نکردهاید.</p>
|
||
<button onClick={() => router.push('/shop')} className="mt-6 text-canina-blue font-black underline underline-offset-8">برو به فروشگاه</button>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{isLoadingOrders ? (
|
||
[1, 2, 3].map(i => <OrderRowSkeleton key={i} />)
|
||
) : (
|
||
(orders || []).map(order => (
|
||
<motion.div
|
||
key={order.id}
|
||
whileHover={{ scale: 1.01, x: -5 }}
|
||
onClick={() => setSelectedOrder(order)}
|
||
className="p-6 border border-medical-gray-100 rounded-[2.5rem] hover:border-canina-blue transition-all group cursor-pointer bg-white overflow-hidden relative"
|
||
>
|
||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-12 h-12 bg-medical-gray-50 rounded-2xl flex items-center justify-center text-canina-blue group-hover:bg-canina-blue group-hover:text-white transition-all shadow-sm">
|
||
<Package className="w-6 h-6" />
|
||
</div>
|
||
<div>
|
||
<div className="text-sm font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors italic">سفارش {toPersian(order.id.toUpperCase())}</div>
|
||
<div className="text-[10px] font-bold text-medical-gray-400 flex items-center gap-1">
|
||
<Calendar className="w-3 h-3 opacity-30" />
|
||
{toPersian(new Date(order.date).toLocaleDateString("fa-IR"))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="text-left">
|
||
<div className="text-lg font-black text-medical-gray-900 italic">{toPersian(order.total.toLocaleString())} <span className="text-xs">تومان</span></div>
|
||
<div className="flex items-center gap-2 justify-end mt-1">
|
||
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase tracking-tighter ${order.status === 'delivered' ? 'bg-green-50 text-green-600' : 'bg-canina-blue/5 text-canina-blue'}`}>
|
||
{order.status === 'delivered' ? 'تحویل شده' : 'ارسال شده'}
|
||
</span>
|
||
<ChevronRight className="w-4 h-4 text-medical-gray-300 -rotate-180" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
))
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "addresses" && (
|
||
<div>
|
||
<div className="flex items-center justify-between mb-8">
|
||
<h3 className="text-2xl font-black text-medical-gray-900 italic">آدرسهای من</h3>
|
||
<button
|
||
onClick={handleAddClick}
|
||
className="px-6 py-3 bg-canina-blue text-white rounded-2xl font-black text-xs flex items-center gap-2 hover:bg-medical-gray-900 transition-all shadow-lg shadow-canina-blue/10"
|
||
>
|
||
<MapPin className="w-4 h-4" />
|
||
افزودن آدرس جدید
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-6">
|
||
{(profile?.addresses || []).length === 0 ? (
|
||
<div className="text-center py-20 border-2 border-dashed border-medical-gray-100 rounded-[3rem]">
|
||
<MapPin className="w-16 h-16 text-medical-gray-200 mx-auto mb-4" />
|
||
<p className="text-medical-gray-400 font-bold">هنوز آدرسی ثبت نکردهاید.</p>
|
||
</div>
|
||
) : (
|
||
(profile?.addresses || []).map((addr) => (
|
||
<motion.div
|
||
key={addr.id}
|
||
initial={{ opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
className={cn(
|
||
"p-8 bg-white border rounded-[2.5rem] relative group transition-all",
|
||
addr.isDefault ? "border-canina-blue shadow-xl shadow-canina-blue/5" : "border-medical-gray-100 hover:border-medical-gray-200"
|
||
)}
|
||
>
|
||
<div className="flex justify-between items-start mb-6">
|
||
<div className="flex items-center gap-4">
|
||
<div className={cn("w-12 h-12 rounded-2xl flex items-center justify-center transition-all", addr.isDefault ? "bg-canina-blue text-white shadow-lg" : "bg-medical-gray-50 text-canina-blue")}>
|
||
<MapPin className="w-6 h-6" />
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-3">
|
||
<h4 className="text-xl font-black text-medical-gray-900 italic">{addr.title}</h4>
|
||
{addr.isDefault && (
|
||
<span className="px-3 py-1 bg-green-50 text-green-600 text-[9px] font-black rounded-full uppercase tracking-widest">پیشفرض</span>
|
||
)}
|
||
</div>
|
||
<p className="text-xs font-bold text-medical-gray-400">تحویل گیرنده: {addr.receptorName}</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => handleEditClick(addr)}
|
||
className="w-10 h-10 flex items-center justify-center rounded-xl bg-medical-gray-50 text-medical-gray-400 hover:bg-canina-blue hover:text-white transition-all shadow-sm"
|
||
>
|
||
<Edit2 className="w-5 h-5" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteClick(addr)}
|
||
className="w-10 h-10 flex items-center justify-center rounded-xl bg-medical-gray-50 text-medical-gray-400 hover:bg-red-500 hover:text-white transition-all shadow-sm"
|
||
>
|
||
<Trash2 className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-medical-gray-50/50 rounded-2xl p-6 mb-6">
|
||
<p className="text-sm font-black text-medical-gray-700 leading-relaxed text-right">{toPersian(addr.province)}، {toPersian(addr.city)}، {toPersian(addr.detail)}</p>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap items-center gap-6">
|
||
<div className="flex items-center gap-2">
|
||
<Phone className="w-4 h-4 text-canina-blue/40" />
|
||
<span className="text-xs font-bold text-medical-gray-500" dir="ltr">{toPersian(addr.phone)}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Hash className="w-4 h-4 text-canina-blue/40" />
|
||
<span className="text-xs font-bold text-medical-gray-500" dir="ltr">{toPersian(addr.zipCode)}</span>
|
||
</div>
|
||
{!addr.isDefault && (
|
||
<button
|
||
onClick={async () => {
|
||
try {
|
||
await setDefaultAddress(addr.id);
|
||
toast.success("آدرس پیشفرض با موفقیت تغییر کرد");
|
||
} catch (err) {
|
||
toast.error("خطا در تغییر آدرس پیشفرض");
|
||
}
|
||
}}
|
||
className="mr-auto text-[10px] font-black text-canina-blue hover:text-medical-gray-900 uppercase tracking-widest whitespace-nowrap"
|
||
>
|
||
انتخاب به عنوان پیشفرض
|
||
</button>
|
||
)}
|
||
{addr.isDefault && (
|
||
<div className="mr-auto flex items-center gap-2 text-green-600 text-[10px] font-black italic">
|
||
<CheckCircle2 className="w-4 h-4" />
|
||
آدرس اصلی ارسال
|
||
</div>
|
||
)}
|
||
</div>
|
||
</motion.div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "wallet" && (
|
||
<div className="space-y-10">
|
||
<div className="flex items-center justify-between">
|
||
<h3 className="text-2xl font-black text-medical-gray-900 italic">مدیریت کیف پول</h3>
|
||
<div className="text-[10px] font-black text-green-600 bg-green-50 px-4 py-2 rounded-full uppercase tracking-widest border border-green-100 flex items-center gap-2">
|
||
<CheckCircle className="w-3 h-3" />
|
||
حساب تایید شده
|
||
</div>
|
||
</div>
|
||
|
||
{/* Main Balance Card */}
|
||
<div className="bg-gradient-to-br from-canina-blue to-blue-600 rounded-[3rem] p-12 text-white shadow-2xl shadow-canina-blue/20 relative overflow-hidden group">
|
||
<div className="absolute top-0 right-0 p-12 opacity-10 group-hover:scale-110 transition-transform duration-700">
|
||
<Wallet className="w-48 h-48" />
|
||
</div>
|
||
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-10">
|
||
<div className="text-center md:text-right">
|
||
<div className="text-xs font-black uppercase tracking-[0.2em] text-white/70 mb-4 flex items-center gap-2 justify-center md:justify-start text-right">
|
||
<div className="w-2 h-2 bg-white rounded-full animate-pulse" />
|
||
موجودی زنده و قابل استفاده
|
||
</div>
|
||
<div className="flex items-baseline gap-4 flex-row-reverse justify-center md:justify-start">
|
||
<div className="text-6xl font-black italic tracking-tight">{toPersian(profile.walletBalance.toLocaleString())}</div>
|
||
<span className="text-xl not-italic font-bold opacity-80 decoration-white/30 underline underline-offset-8">تومان</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-4 w-full md:w-auto">
|
||
<div className="relative group/btn flex-1 md:flex-none">
|
||
<button
|
||
disabled
|
||
className="w-full md:w-40 h-16 bg-white/10 backdrop-blur-md rounded-2xl font-black text-sm flex items-center justify-center gap-2 border border-white/20 opacity-50 cursor-not-allowed"
|
||
>
|
||
<ArrowDownCircle className="w-5 h-5" />
|
||
برداشت وجه
|
||
</button>
|
||
<div className="absolute top-full right-0 mt-3 w-48 p-3 bg-medical-gray-900 text-white text-[10px] font-bold rounded-xl opacity-0 group-hover/btn:opacity-100 transition-all pointer-events-none z-20 text-center shadow-xl">
|
||
قابلیت برداشت وجه بهزودی فعال خواهد شد.
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => setIsTopUpModalOpen(true)}
|
||
className="flex-1 md:w-40 h-16 bg-white text-canina-blue rounded-2xl font-black text-sm flex items-center justify-center gap-2 hover:bg-medical-gray-900 hover:text-white transition-all shadow-xl shadow-black/10 group/topup"
|
||
>
|
||
<ArrowUpCircle className="w-5 h-5 group-hover:-translate-y-1 transition-transform" />
|
||
شارژ آنی
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Transaction History */}
|
||
<div className="pt-10 border-t border-medical-gray-100">
|
||
<div className="flex items-center gap-4 mb-8">
|
||
<div className="w-10 h-10 bg-medical-gray-50 rounded-xl flex items-center justify-center text-medical-gray-400">
|
||
<Clock className="w-5 h-5" />
|
||
</div>
|
||
<h4 className="text-lg font-black text-medical-gray-900 italic">تاریخچه تراکنشها</h4>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
{(profile?.transactions || []).length === 0 ? (
|
||
<div className="text-center py-20 bg-medical-gray-50 rounded-[2.5rem] border border-dashed border-medical-gray-200">
|
||
<Info className="w-12 h-12 text-medical-gray-200 mx-auto mb-4" />
|
||
<p className="text-medical-gray-400 font-bold">تراکنشی یافت نشد.</p>
|
||
</div>
|
||
) : (
|
||
(profile?.transactions || []).map((trx) => (
|
||
<div key={trx.id} className="p-6 bg-white border border-medical-gray-100 rounded-[2rem] flex flex-wrap items-center justify-between gap-6 hover:shadow-lg hover:border-canina-blue/10 transition-all group">
|
||
<div className="flex items-center gap-4">
|
||
<div className={cn(
|
||
"w-12 h-12 rounded-2xl flex items-center justify-center transition-colors",
|
||
trx.type === 'top_up' ? "bg-green-50 text-green-500 group-hover:bg-green-500 group-hover:text-white" : "bg-red-50 text-red-500 group-hover:bg-red-500 group-hover:text-white"
|
||
)}>
|
||
{trx.type === 'top_up' ? <ArrowUpCircle className="w-6 h-6" /> : <ShoppingCart className="w-6 h-6" />}
|
||
</div>
|
||
<div>
|
||
<div className="text-sm font-black text-medical-gray-900">{trx.type === 'top_up' ? "افزایش موجودی (شارژ)" : "پرداخت سفارش"}</div>
|
||
<div className="text-[10px] font-bold text-medical-gray-400 flex items-center gap-2">
|
||
<span>کد پیگیری: {trx.id}</span>
|
||
<span className="w-1 h-1 bg-medical-gray-200 rounded-full" />
|
||
<span>{toPersian(new Date(trx.date).toLocaleDateString("fa-IR"))}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="text-left">
|
||
<div className={cn(
|
||
"text-lg font-black italic",
|
||
trx.type === 'top_up' ? "text-green-600" : "text-red-500"
|
||
)}>
|
||
{trx.type === 'top_up' ? "+" : ""}{toPersian(trx.amount.toLocaleString())} <span className="text-xs not-italic">تومان</span>
|
||
</div>
|
||
<div className="text-[9px] font-black uppercase text-green-500 tracking-widest mt-1">
|
||
تایید شده
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</motion.div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<OrderDetailsModal
|
||
isOpen={!!selectedOrder}
|
||
onClose={() => setSelectedOrder(null)}
|
||
order={selectedOrder}
|
||
/>
|
||
<AddressModal
|
||
isOpen={isAddressModalOpen}
|
||
onClose={() => setIsAddressModalOpen(false)}
|
||
onSave={handleSaveAddress}
|
||
editingAddress={editingAddress}
|
||
/>
|
||
<DeleteConfirmModal
|
||
isOpen={isDeleteModalOpen}
|
||
onClose={() => setIsDeleteModalOpen(false)}
|
||
onConfirm={handleConfirmDelete}
|
||
title="حذف آدرس"
|
||
message={`آیا از حذف آدرس "${addressToDelete?.title}" اطمینان دارید؟ این عمل غیرقابل بازگشت است.`}
|
||
/>
|
||
<TopUpModal
|
||
isOpen={isTopUpModalOpen}
|
||
onClose={() => setIsTopUpModalOpen(false)}
|
||
onConfirm={topUpWallet}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|