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)
|
## 📍 بخش ۳: استانداردسازی دکمه بازگشت (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)
|
## 📍 بخش ۵: داشبورد کاربر و کیف پول (User Dashboard & Wallet Security)
|
||||||
- [ ] **TASK-5.1 [Frontend Protection]:** جلوگیری از دسترسی کاربران غیرلاگین به صفحه داشبورد و هدایت خودکار به صفحه ورود.
|
- [x] **TASK-5.1 [Frontend Protection]:** جلوگیری از دسترسی کاربران غیرلاگین به صفحه داشبورد و هدایت خودکار به صفحه ورود.
|
||||||
- [ ] **TASK-5.2 [Frontend Wallet UX]:** رفع مشکل نمایش پیام موفقیت کاذب هنگام شارژ کیف پول بدون لاگین و بهینهسازی سرعت فرآیند شارژ.
|
- [ ] **TASK-5.2 [Frontend Wallet UX]:** رفع مشکل نمایش پیام موفقیت کاذب هنگام شارژ کیف پول بدون لاگین و بهینهسازی سرعت فرآیند شارژ.
|
||||||
- [ ] **TASK-5.3 [Frontend & Backend Wallet]:** اصلاح استایل و Layout شارژ کیف پول برای اعداد بزرگ + فعالسازی منطق دکمه درخواست برداشت وجه (ثبت درخواست + ارسال SMS به کاربر و مدیر + مدیریت در پنل ادمین).
|
- [ ] **TASK-5.3 [Frontend & Backend Wallet]:** اصلاح استایل و Layout شارژ کیف پول برای اعداد بزرگ + فعالسازی منطق دکمه درخواست برداشت وجه (ثبت درخواست + ارسال SMS به کاربر و مدیر + مدیریت در پنل ادمین).
|
||||||
- [ ] **TASK-5.4 [Frontend UX]:** بررسی و رفع نمایش نشان "حساب تایید شده" برای کاربران غیر لاگین.
|
- [ ] **TASK-5.4 [Frontend UX]:** بررسی و رفع نمایش نشان "حساب تایید شده" برای کاربران غیر لاگین.
|
||||||
@ -90,7 +90,7 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 📍 بخش ۸: رفع باگ عنوان صفحه محصول (Product Page Title Fix)
|
## 📍 بخش ۸: رفع باگ عنوان صفحه محصول (Product Page Title Fix)
|
||||||
- [ ] **TASK-8.1 [Frontend Site Fix]:** رفع باگ عنوان صفحه محصول (عدم نمایش متن "محصول یافت نشد!" هنگام لود شدن دیتای محصول).
|
- [x] **TASK-8.1 [Frontend Site Fix]:** رفع باگ عنوان صفحه محصول (عدم نمایش متن "محصول یافت نشد!" هنگام لود شدن دیتای محصول).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -104,8 +104,8 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 📍 بخش ۱۰: بهینهسازی ورود با OTP و خواندن خودکار SMS
|
## 📍 بخش ۱۰: بهینهسازی ورود با OTP و خواندن خودکار SMS
|
||||||
- [ ] **TASK-10.1 [Frontend OTP UX]:** پشتیبانی کامل از WebOTP API جهت خواندن خودکار کد SMS در موبایل.
|
- [x] **TASK-10.1 [Frontend OTP UX]:** پشتیبانی کامل از WebOTP API جهت خواندن خودکار کد SMS در موبایل.
|
||||||
- [ ] **TASK-10.2 [Frontend OTP UX]:** پشتیبانی از Paste یکباره کد ۵ رقمی روی اولین input و توزیع خودکار آن در سایر خانهها.
|
- [x] **TASK-10.2 [Frontend OTP UX]:** پشتیبانی از Paste یکباره کد ۵ رقمی روی اولین input و توزیع خودکار آن در سایر خانهها.
|
||||||
- [ ] **TASK-10.3 [Frontend OTP UX]:** ارسال خودکار (Auto Submit) به محض پر شدن خانه پنجم و امکان ویرایش سریع در صورت نادرست بودن کد.
|
- [x] **TASK-10.3 [Frontend OTP UX]:** ارسال خودکار (Auto Submit) به محض پر شدن خانه پنجم و امکان ویرایش سریع در صورت نادرست بودن کد.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Search, Plus, Edit2, Trash2, FileText, Image as ImageIcon, CheckCircle2, XCircle } from 'lucide-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 api, { BASE_DOMAIN } from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
|
|
||||||
export default function Blogs() {
|
export default function Blogs() {
|
||||||
const [blogs, setBlogs] = useState<any[]>([]);
|
const [blogs, setBlogs] = useState<any[]>([]);
|
||||||
@ -16,7 +18,8 @@ export default function Blogs() {
|
|||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
||||||
const [editingBlog, setEditingBlog] = useState<any>(null);
|
const [editingBlog, setEditingBlog] = useState<any>(null);
|
||||||
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
title: '',
|
title: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
@ -55,24 +58,30 @@ export default function Blogs() {
|
|||||||
try {
|
try {
|
||||||
if (editingBlog) {
|
if (editingBlog) {
|
||||||
await api.put(`/admin/blogs/${editingBlog.id}`, formData);
|
await api.put(`/admin/blogs/${editingBlog.id}`, formData);
|
||||||
|
toast.success('مقاله با موفقیت ویرایش شد');
|
||||||
} else {
|
} else {
|
||||||
await api.post('/admin/blogs', formData);
|
await api.post('/admin/blogs', formData);
|
||||||
|
toast.success('مقاله جدید با موفقیت ذخیره شد');
|
||||||
}
|
}
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
fetchBlogs();
|
fetchBlogs();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Save failed', error);
|
console.error('Save failed', error);
|
||||||
alert('خطا در ذخیره مقاله');
|
toast.error('خطا در ذخیره مقاله: ' + (error.response?.data?.message || error.message || ''));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const confirmDelete = async () => {
|
||||||
if (!window.confirm('آیا از حذف این مقاله مطمئن هستید؟')) return;
|
if (!deleteTargetId) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`/admin/blogs/${id}`);
|
await api.delete(`/admin/blogs/${deleteTargetId}`);
|
||||||
|
toast.success('مقاله با موفقیت حذف شد');
|
||||||
fetchBlogs();
|
fetchBlogs();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Delete failed', error);
|
console.error('Delete failed', error);
|
||||||
|
toast.error('خطا در حذف مقاله');
|
||||||
|
} finally {
|
||||||
|
setDeleteTargetId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Search, Plus, Edit2, Trash2, FolderTree, Image as ImageIcon, X } from 'lucide-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 api, { BASE_DOMAIN } from '../services/api';
|
||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
|
|
||||||
export default function Categories() {
|
export default function Categories() {
|
||||||
const [categories, setCategories] = useState<any[]>([]);
|
const [categories, setCategories] = useState<any[]>([]);
|
||||||
@ -16,7 +18,8 @@ export default function Categories() {
|
|||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
||||||
const [editingCategory, setEditingCategory] = useState<any>(null);
|
const [editingCategory, setEditingCategory] = useState<any>(null);
|
||||||
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
@ -54,24 +57,30 @@ export default function Categories() {
|
|||||||
try {
|
try {
|
||||||
if (editingCategory) {
|
if (editingCategory) {
|
||||||
await api.put(`/admin/categories/${editingCategory.id}`, formData);
|
await api.put(`/admin/categories/${editingCategory.id}`, formData);
|
||||||
|
toast.success('دستهبندی با موفقیت ویرایش شد');
|
||||||
} else {
|
} else {
|
||||||
await api.post('/admin/categories', formData);
|
await api.post('/admin/categories', formData);
|
||||||
|
toast.success('دستهبندی جدید با موفقیت ایجاد شد');
|
||||||
}
|
}
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
fetchCategories();
|
fetchCategories();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Save failed', error);
|
console.error('Save failed', error);
|
||||||
alert('خطا در ذخیره دستهبندی');
|
toast.error('خطا در ذخیره دستهبندی: ' + (error.response?.data?.message || error.message || ''));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const confirmDelete = async () => {
|
||||||
if (!window.confirm('آیا مطمئن هستید؟')) return;
|
if (!deleteTargetId) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`/admin/categories/${id}`);
|
await api.delete(`/admin/categories/${deleteTargetId}`);
|
||||||
|
toast.success('دستهبندی با موفقیت حذف شد');
|
||||||
fetchCategories();
|
fetchCategories();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Delete failed', error);
|
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">
|
<button onClick={() => openModal(cat)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</button>
|
</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" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -318,6 +327,14 @@ export default function Categories() {
|
|||||||
onClose={() => setIsMediaSelectorOpen(false)}
|
onClose={() => setIsMediaSelectorOpen(false)}
|
||||||
onSelect={(url) => setFormData({...formData, imageUrl: url})}
|
onSelect={(url) => setFormData({...formData, imageUrl: url})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={!!deleteTargetId}
|
||||||
|
title="حذف دستهبندی"
|
||||||
|
message="آیا از حذف این دستهبندی مطمئن هستید؟ این عملیات قابل بازگشت نیست."
|
||||||
|
onConfirm={confirmDelete}
|
||||||
|
onCancel={() => setDeleteTargetId(null)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import api, { BASE_DOMAIN } from '../services/api';
|
|||||||
import Spinner from '../components/ui/Spinner';
|
import Spinner from '../components/ui/Spinner';
|
||||||
import Pagination from '../components/ui/Pagination';
|
import Pagination from '../components/ui/Pagination';
|
||||||
import MediaSelector from '../components/ui/MediaSelector';
|
import MediaSelector from '../components/ui/MediaSelector';
|
||||||
|
import ConfirmModal from '../components/ui/ConfirmModal';
|
||||||
|
|
||||||
export default function Products() {
|
export default function Products() {
|
||||||
const [products, setProducts] = useState<any[]>([]);
|
const [products, setProducts] = useState<any[]>([]);
|
||||||
@ -54,6 +55,7 @@ export default function Products() {
|
|||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
const [isMediaSelectorOpen, setIsMediaSelectorOpen] = useState(false);
|
||||||
const [editingProduct, setEditingProduct] = useState<any>(null);
|
const [editingProduct, setEditingProduct] = useState<any>(null);
|
||||||
|
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState('general');
|
const [activeTab, setActiveTab] = useState('general');
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@ -65,6 +67,7 @@ export default function Products() {
|
|||||||
shortDescription: '',
|
shortDescription: '',
|
||||||
categoryId: '',
|
categoryId: '',
|
||||||
priceValue: 0,
|
priceValue: 0,
|
||||||
|
wholesalePrice: '' as number | string,
|
||||||
priceDisplay: '',
|
priceDisplay: '',
|
||||||
unit: '',
|
unit: '',
|
||||||
packageSize: 0,
|
packageSize: 0,
|
||||||
@ -169,26 +172,32 @@ export default function Products() {
|
|||||||
|
|
||||||
if (editingProduct) {
|
if (editingProduct) {
|
||||||
await api.put(`/admin/products/${editingProduct.id}`, finalPayload);
|
await api.put(`/admin/products/${editingProduct.id}`, finalPayload);
|
||||||
|
toast.success('محصول با موفقیت ویرایش شد');
|
||||||
} else {
|
} else {
|
||||||
await api.post('/admin/products', finalPayload);
|
await api.post('/admin/products', finalPayload);
|
||||||
|
toast.success('محصول با موفقیت ایجاد شد');
|
||||||
}
|
}
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
setImageErrors({});
|
setImageErrors({});
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Save failed', error);
|
console.error('Save failed', error);
|
||||||
alert('خطا در ذخیره محصول');
|
toast.error('خطا در ذخیره محصول: ' + (error.response?.data?.message || error.message || ''));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const confirmDelete = async () => {
|
||||||
if (!window.confirm('آیا از حذف این محصول مطمئن هستید؟')) return;
|
if (!deleteTargetId) return;
|
||||||
try {
|
try {
|
||||||
await api.delete(`/admin/products/${id}`);
|
await api.delete(`/admin/products/${deleteTargetId}`);
|
||||||
|
toast.success('محصول با موفقیت حذف شد');
|
||||||
setImageErrors({});
|
setImageErrors({});
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Delete failed', error);
|
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">
|
<button onClick={() => openModal(product)} className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors">
|
||||||
<Edit2 className="w-4 h-4" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</button>
|
</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" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -810,6 +819,14 @@ export default function Products() {
|
|||||||
}}
|
}}
|
||||||
selectedUrl={formData.imageUrl.startsWith('http') ? formData.imageUrl : formData.imageUrl ? `${BASE_DOMAIN}${formData.imageUrl}` : undefined}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import SafeImage from "../../components/SafeImage";
|
import SafeImage from "../../components/SafeImage";
|
||||||
|
import BackButton from "../../components/BackButton";
|
||||||
import { Download, Printer, ShieldCheck, Sparkles, CheckCircle2, ChevronLeft, Search, Filter } from "lucide-react";
|
import { Download, Printer, ShieldCheck, Sparkles, CheckCircle2, ChevronLeft, Search, Filter } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { productService } from "../../lib/services/productService";
|
import { productService } from "../../lib/services/productService";
|
||||||
import { Product } from "../../lib/data/products";
|
import { Product } from "../../lib/data/products";
|
||||||
import { toPersian } from "../../lib/utils";
|
import { toPersian } from "../../lib/utils";
|
||||||
|
|
||||||
|
|
||||||
export default function CatalogClient() {
|
export default function CatalogClient() {
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -89,6 +91,7 @@ export default function CatalogClient() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
<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
|
<button
|
||||||
onClick={handlePrint}
|
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"
|
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) {
|
if (!product) {
|
||||||
return {
|
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]);
|
}, [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
|
// Reset modal state when closed or opened
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
@ -79,26 +137,7 @@ export default function LoginModal({ isOpen, onClose, onLogin, petName, isAdviso
|
|||||||
|
|
||||||
const handleVerifyOtp = async (e: React.FormEvent) => {
|
const handleVerifyOtp = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const cleanCode = otpCode.trim();
|
handleVerifyOtpWithCode(otpCode);
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResendOtp = async () => {
|
const handleResendOtp = async () => {
|
||||||
@ -234,9 +273,11 @@ export default function LoginModal({ isOpen, onClose, onLogin, petName, isAdviso
|
|||||||
ref={otpInputRef}
|
ref={otpInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
maxLength={5}
|
maxLength={5}
|
||||||
|
autoComplete="one-time-code"
|
||||||
placeholder="کد ۵ رقمی"
|
placeholder="کد ۵ رقمی"
|
||||||
value={otpCode}
|
value={otpCode}
|
||||||
onChange={(e) => setOtpCode(e.target.value.replace(/[^0-9]/g, ''))}
|
onChange={handleOtpInputChange}
|
||||||
|
onPaste={handleOtpPaste}
|
||||||
disabled={isLoading}
|
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]"
|
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() {
|
export default function UserDashboard() {
|
||||||
const router = useRouter();
|
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 { orders } = useCartStore();
|
||||||
const [activeTab, setActiveTab] = React.useState<"profile" | "orders" | "wallet" | "addresses" | "tickets">("profile");
|
const [activeTab, setActiveTab] = React.useState<"profile" | "orders" | "wallet" | "addresses" | "tickets">("profile");
|
||||||
const [isLoadingOrders, setIsLoadingOrders] = useState(false);
|
const [isLoadingOrders, setIsLoadingOrders] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
// Fetch fresh profile data (which includes orders) whenever orders tab is opened
|
// Fetch fresh profile data (which includes orders) whenever orders tab is opened
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (activeTab === "orders") {
|
if (activeTab === "orders") {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user