feat: add BackButton, protect UserDashboard auth, fix product loading metadata title and add WebOTP auto-submit
This commit is contained in:
parent
2ce24b0473
commit
9870516256
@ -62,7 +62,7 @@
|
||||
---
|
||||
|
||||
## 📍 بخش ۳: استانداردسازی دکمه بازگشت (Standard Back Button)
|
||||
- [ ] **TASK-3.1 [Frontend Site UI]:** پیادهسازی کامپوننت استاندارد و اتمیک BackButton با استفاده از `router.back()` و حفظ دقیق موقعیت Scroll صفحه قبلی کاربر.
|
||||
- [x] **TASK-3.1 [Frontend Site UI]:** پیادهسازی کامپوننت استاندارد و اتمیک BackButton با استفاده از `router.back()` و حفظ دقیق موقعیت Scroll صفحه قبلی کاربر.
|
||||
|
||||
---
|
||||
|
||||
@ -72,7 +72,7 @@
|
||||
---
|
||||
|
||||
## 📍 بخش ۵: داشبورد کاربر و کیف پول (User Dashboard & Wallet Security)
|
||||
- [ ] **TASK-5.1 [Frontend Protection]:** جلوگیری از دسترسی کاربران غیرلاگین به صفحه داشبورد و هدایت خودکار به صفحه ورود.
|
||||
- [x] **TASK-5.1 [Frontend Protection]:** جلوگیری از دسترسی کاربران غیرلاگین به صفحه داشبورد و هدایت خودکار به صفحه ورود.
|
||||
- [ ] **TASK-5.2 [Frontend Wallet UX]:** رفع مشکل نمایش پیام موفقیت کاذب هنگام شارژ کیف پول بدون لاگین و بهینهسازی سرعت فرآیند شارژ.
|
||||
- [ ] **TASK-5.3 [Frontend & Backend Wallet]:** اصلاح استایل و Layout شارژ کیف پول برای اعداد بزرگ + فعالسازی منطق دکمه درخواست برداشت وجه (ثبت درخواست + ارسال SMS به کاربر و مدیر + مدیریت در پنل ادمین).
|
||||
- [ ] **TASK-5.4 [Frontend UX]:** بررسی و رفع نمایش نشان "حساب تایید شده" برای کاربران غیر لاگین.
|
||||
@ -90,7 +90,7 @@
|
||||
---
|
||||
|
||||
## 📍 بخش ۸: رفع باگ عنوان صفحه محصول (Product Page Title Fix)
|
||||
- [ ] **TASK-8.1 [Frontend Site Fix]:** رفع باگ عنوان صفحه محصول (عدم نمایش متن "محصول یافت نشد!" هنگام لود شدن دیتای محصول).
|
||||
- [x] **TASK-8.1 [Frontend Site Fix]:** رفع باگ عنوان صفحه محصول (عدم نمایش متن "محصول یافت نشد!" هنگام لود شدن دیتای محصول).
|
||||
|
||||
---
|
||||
|
||||
@ -104,8 +104,8 @@
|
||||
---
|
||||
|
||||
## 📍 بخش ۱۰: بهینهسازی ورود با OTP و خواندن خودکار SMS
|
||||
- [ ] **TASK-10.1 [Frontend OTP UX]:** پشتیبانی کامل از WebOTP API جهت خواندن خودکار کد SMS در موبایل.
|
||||
- [ ] **TASK-10.2 [Frontend OTP UX]:** پشتیبانی از Paste یکباره کد ۵ رقمی روی اولین input و توزیع خودکار آن در سایر خانهها.
|
||||
- [ ] **TASK-10.3 [Frontend OTP UX]:** ارسال خودکار (Auto Submit) به محض پر شدن خانه پنجم و امکان ویرایش سریع در صورت نادرست بودن کد.
|
||||
- [x] **TASK-10.1 [Frontend OTP UX]:** پشتیبانی کامل از WebOTP API جهت خواندن خودکار کد SMS در موبایل.
|
||||
- [x] **TASK-10.2 [Frontend OTP UX]:** پشتیبانی از Paste یکباره کد ۵ رقمی روی اولین input و توزیع خودکار آن در سایر خانهها.
|
||||
- [x] **TASK-10.3 [Frontend OTP UX]:** ارسال خودکار (Auto Submit) به محض پر شدن خانه پنجم و امکان ویرایش سریع در صورت نادرست بودن کد.
|
||||
|
||||
---
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Search, Plus, Edit2, Trash2, FileText, Image as ImageIcon, CheckCircle2, XCircle } from 'lucide-react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import api, { BASE_DOMAIN } from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
import MediaSelector from '../components/ui/MediaSelector';
|
||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||
|
||||
export default function Blogs() {
|
||||
const [blogs, setBlogs] = useState<any[]>([]);
|
||||
@ -16,7 +18,8 @@ export default function Blogs() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
||||
const [editingBlog, setEditingBlog] = useState<any>(null);
|
||||
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
slug: '',
|
||||
@ -55,24 +58,30 @@ export default function Blogs() {
|
||||
try {
|
||||
if (editingBlog) {
|
||||
await api.put(`/admin/blogs/${editingBlog.id}`, formData);
|
||||
toast.success('مقاله با موفقیت ویرایش شد');
|
||||
} else {
|
||||
await api.post('/admin/blogs', formData);
|
||||
toast.success('مقاله جدید با موفقیت ذخیره شد');
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
fetchBlogs();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Save failed', error);
|
||||
alert('خطا در ذخیره مقاله');
|
||||
toast.error('خطا در ذخیره مقاله: ' + (error.response?.data?.message || error.message || ''));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!window.confirm('آیا از حذف این مقاله مطمئن هستید؟')) return;
|
||||
const confirmDelete = async () => {
|
||||
if (!deleteTargetId) return;
|
||||
try {
|
||||
await api.delete(`/admin/blogs/${id}`);
|
||||
await api.delete(`/admin/blogs/${deleteTargetId}`);
|
||||
toast.success('مقاله با موفقیت حذف شد');
|
||||
fetchBlogs();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Delete failed', error);
|
||||
toast.error('خطا در حذف مقاله');
|
||||
} finally {
|
||||
setDeleteTargetId(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Search, Plus, Edit2, Trash2, FolderTree, Image as ImageIcon, X } from 'lucide-react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import api, { BASE_DOMAIN } from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
import MediaSelector from '../components/ui/MediaSelector';
|
||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||
|
||||
export default function Categories() {
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
@ -16,7 +18,8 @@ export default function Categories() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
||||
const [editingCategory, setEditingCategory] = useState<any>(null);
|
||||
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
slug: '',
|
||||
@ -54,24 +57,30 @@ export default function Categories() {
|
||||
try {
|
||||
if (editingCategory) {
|
||||
await api.put(`/admin/categories/${editingCategory.id}`, formData);
|
||||
toast.success('دستهبندی با موفقیت ویرایش شد');
|
||||
} else {
|
||||
await api.post('/admin/categories', formData);
|
||||
toast.success('دستهبندی جدید با موفقیت ایجاد شد');
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
fetchCategories();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Save failed', error);
|
||||
alert('خطا در ذخیره دستهبندی');
|
||||
toast.error('خطا در ذخیره دستهبندی: ' + (error.response?.data?.message || error.message || ''));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!window.confirm('آیا مطمئن هستید؟')) return;
|
||||
const confirmDelete = async () => {
|
||||
if (!deleteTargetId) return;
|
||||
try {
|
||||
await api.delete(`/admin/categories/${id}`);
|
||||
await api.delete(`/admin/categories/${deleteTargetId}`);
|
||||
toast.success('دستهبندی با موفقیت حذف شد');
|
||||
fetchCategories();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Delete failed', error);
|
||||
toast.error('خطا در حذف دستهبندی');
|
||||
} finally {
|
||||
setDeleteTargetId(null);
|
||||
}
|
||||
};
|
||||
|
||||
@ -179,7 +188,7 @@ export default function Categories() {
|
||||
<button onClick={() => openModal(cat)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(cat.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||
<button onClick={() => setDeleteTargetId(cat.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@ -318,6 +327,14 @@ export default function Categories() {
|
||||
onClose={() => setIsMediaSelectorOpen(false)}
|
||||
onSelect={(url) => setFormData({...formData, imageUrl: url})}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={!!deleteTargetId}
|
||||
title="حذف دستهبندی"
|
||||
message="آیا از حذف این دستهبندی مطمئن هستید؟ این عملیات قابل بازگشت نیست."
|
||||
onConfirm={confirmDelete}
|
||||
onCancel={() => setDeleteTargetId(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import api, { BASE_DOMAIN } from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
import Pagination from '../components/ui/Pagination';
|
||||
import MediaSelector from '../components/ui/MediaSelector';
|
||||
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||
|
||||
export default function Products() {
|
||||
const [products, setProducts] = useState<any[]>([]);
|
||||
@ -54,6 +55,7 @@ export default function Products() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
||||
const [editingProduct, setEditingProduct] = useState<any>(null);
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('general');
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
@ -65,6 +67,7 @@ export default function Products() {
|
||||
shortDescription: '',
|
||||
categoryId: '',
|
||||
priceValue: 0,
|
||||
wholesalePrice: '' as number | string,
|
||||
priceDisplay: '',
|
||||
unit: '',
|
||||
packageSize: 0,
|
||||
@ -169,26 +172,32 @@ export default function Products() {
|
||||
|
||||
if (editingProduct) {
|
||||
await api.put(`/admin/products/${editingProduct.id}`, finalPayload);
|
||||
toast.success('محصول با موفقیت ویرایش شد');
|
||||
} else {
|
||||
await api.post('/admin/products', finalPayload);
|
||||
toast.success('محصول با موفقیت ایجاد شد');
|
||||
}
|
||||
setIsModalOpen(false);
|
||||
setImageErrors({});
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Save failed', error);
|
||||
alert('خطا در ذخیره محصول');
|
||||
toast.error('خطا در ذخیره محصول: ' + (error.response?.data?.message || error.message || ''));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!window.confirm('آیا از حذف این محصول مطمئن هستید؟')) return;
|
||||
const confirmDelete = async () => {
|
||||
if (!deleteTargetId) return;
|
||||
try {
|
||||
await api.delete(`/admin/products/${id}`);
|
||||
await api.delete(`/admin/products/${deleteTargetId}`);
|
||||
toast.success('محصول با موفقیت حذف شد');
|
||||
setImageErrors({});
|
||||
fetchData();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('Delete failed', error);
|
||||
toast.error('خطا در حذف محصول');
|
||||
} finally {
|
||||
setDeleteTargetId(null);
|
||||
}
|
||||
};
|
||||
|
||||
@ -297,7 +306,7 @@ export default function Products() {
|
||||
<button onClick={() => openModal(product)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(product.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||
<button onClick={() => setDeleteTargetId(product.id)} className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@ -810,6 +819,14 @@ export default function Products() {
|
||||
}}
|
||||
selectedUrl={formData.imageUrl.startsWith('http') ? formData.imageUrl : formData.imageUrl ? `${BASE_DOMAIN}${formData.imageUrl}` : undefined}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={!!deleteTargetId}
|
||||
title="حذف محصول"
|
||||
message="آیا از حذف این محصول مطمئن هستید؟ این عملیات قابل بازگشت نیست."
|
||||
onConfirm={confirmDelete}
|
||||
onCancel={() => setDeleteTargetId(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,12 +2,14 @@
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import SafeImage from "../../components/SafeImage";
|
||||
import BackButton from "../../components/BackButton";
|
||||
import { Download, Printer, ShieldCheck, Sparkles, CheckCircle2, ChevronLeft, Search, Filter } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { productService } from "../../lib/services/productService";
|
||||
import { Product } from "../../lib/data/products";
|
||||
import { toPersian } from "../../lib/utils";
|
||||
|
||||
|
||||
export default function CatalogClient() {
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@ -89,6 +91,7 @@ export default function CatalogClient() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<BackButton className="bg-white/10 text-white border-white/20 hover:bg-white/20 hover:text-white" />
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="bg-amber-400 hover:bg-amber-500 text-slate-900 font-black px-6 py-4 rounded-2xl shadow-xl hover:scale-105 transition-all flex items-center gap-3 text-sm cursor-pointer"
|
||||
|
||||
@ -11,7 +11,7 @@ export async function generateMetadata(
|
||||
|
||||
if (!product) {
|
||||
return {
|
||||
title: 'محصول یافت نشد',
|
||||
title: 'خرید مکمل درمانی پت | کانینا ایران',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
38
frontend/application/components/BackButton.tsx
Normal file
38
frontend/application/components/BackButton.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
interface BackButtonProps {
|
||||
className?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export default function BackButton({ className, label = "بازگشت" }: BackButtonProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const handleBack = () => {
|
||||
// router.back() preserves browser history and scroll position natively
|
||||
if (typeof window !== "undefined" && window.history.length > 1) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push("/");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBack}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 px-4 py-2 bg-white/80 backdrop-blur-sm border border-medical-gray-200 hover:border-canina-blue hover:text-canina-blue text-medical-gray-700 text-xs font-black rounded-xl transition-all shadow-xs active:scale-95 whitespace-nowrap cursor-pointer",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@ -33,6 +33,64 @@ export default function LoginModal({ isOpen, onClose, onLogin, petName, isAdviso
|
||||
}
|
||||
}, [step]);
|
||||
|
||||
// WebOTP API & Auto-Submit
|
||||
useEffect(() => {
|
||||
if (step === "otp" && typeof window !== "undefined" && "OTPCredential" in window) {
|
||||
const ac = new AbortController();
|
||||
(navigator as any).credentials
|
||||
.get({
|
||||
otp: { transport: ["sms"] },
|
||||
signal: ac.signal,
|
||||
})
|
||||
.then((otp: any) => {
|
||||
if (otp && otp.code) {
|
||||
setOtpCode(otp.code);
|
||||
handleVerifyOtpWithCode(otp.code);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return () => ac.abort();
|
||||
}
|
||||
}, [step]);
|
||||
|
||||
const handleVerifyOtpWithCode = async (code: string) => {
|
||||
const cleanCode = code.trim();
|
||||
if (cleanCode.length !== 5) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await authService.verifyOtp(phoneNumber.trim(), cleanCode);
|
||||
if (response.success) {
|
||||
await fetchProfile();
|
||||
toast.success("ورود با موفقیت انجام شد");
|
||||
onLogin();
|
||||
onClose();
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || "کد تایید اشتباه است");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOtpInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value.replace(/[^0-9]/g, "").slice(0, 5);
|
||||
setOtpCode(val);
|
||||
if (val.length === 5) {
|
||||
handleVerifyOtpWithCode(val);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOtpPaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
|
||||
const pasted = e.clipboardData.getData("text").replace(/[^0-9]/g, "").slice(0, 5);
|
||||
if (pasted.length === 5) {
|
||||
e.preventDefault();
|
||||
setOtpCode(pasted);
|
||||
handleVerifyOtpWithCode(pasted);
|
||||
}
|
||||
};
|
||||
|
||||
// Reset modal state when closed or opened
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
@ -79,26 +137,7 @@ export default function LoginModal({ isOpen, onClose, onLogin, petName, isAdviso
|
||||
|
||||
const handleVerifyOtp = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const cleanCode = otpCode.trim();
|
||||
if (cleanCode.length !== 5) {
|
||||
toast.error("کد تایید باید ۵ رقم باشد");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await authService.verifyOtp(phoneNumber.trim(), cleanCode);
|
||||
if (response.success) {
|
||||
await fetchProfile();
|
||||
toast.success("ورود با موفقیت انجام شد");
|
||||
onLogin();
|
||||
onClose();
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(err.message || "کد تایید اشتباه است");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
handleVerifyOtpWithCode(otpCode);
|
||||
};
|
||||
|
||||
const handleResendOtp = async () => {
|
||||
@ -234,9 +273,11 @@ export default function LoginModal({ isOpen, onClose, onLogin, petName, isAdviso
|
||||
ref={otpInputRef}
|
||||
type="text"
|
||||
maxLength={5}
|
||||
autoComplete="one-time-code"
|
||||
placeholder="کد ۵ رقمی"
|
||||
value={otpCode}
|
||||
onChange={(e) => setOtpCode(e.target.value.replace(/[^0-9]/g, ''))}
|
||||
onChange={handleOtpInputChange}
|
||||
onPaste={handleOtpPaste}
|
||||
disabled={isLoading}
|
||||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 pr-12 pl-4 focus:ring-2 focus:ring-canina-blue/20 focus:border-canina-blue outline-none font-black text-2xl text-center tracking-[0.5em]"
|
||||
/>
|
||||
|
||||
@ -16,11 +16,20 @@ import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function UserDashboard() {
|
||||
const router = useRouter();
|
||||
const { profile, logout, addAddress, updateAddress, deleteAddress, setDefaultAddress, topUpWallet } = useUserStore();
|
||||
const { profile, isLoggedIn, logout, addAddress, updateAddress, deleteAddress, setDefaultAddress, topUpWallet } = useUserStore();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isLoggedIn && typeof window !== "undefined") {
|
||||
router.push("/");
|
||||
toast.error("برای دسترسی به داشبورد لطفاً ابتدا وارد حساب کاربری خود شوید.");
|
||||
}
|
||||
}, [isLoggedIn, router]);
|
||||
|
||||
const { orders } = useCartStore();
|
||||
const [activeTab, setActiveTab] = React.useState<"profile" | "orders" | "wallet" | "addresses" | "tickets">("profile");
|
||||
const [isLoadingOrders, setIsLoadingOrders] = useState(false);
|
||||
|
||||
|
||||
// Fetch fresh profile data (which includes orders) whenever orders tab is opened
|
||||
React.useEffect(() => {
|
||||
if (activeTab === "orders") {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user