fix(lint&types): resolve all lint warnings, typecheck errors and effect state updates across frontend application and admin panel

This commit is contained in:
parsa aghaei 2026-08-03 10:13:11 +03:30
parent 920dd532e8
commit c1b2f416ed
29 changed files with 442 additions and 352 deletions

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { X, Upload, Image as ImageIcon, Trash2, CheckCircle2, Clipboard } from 'lucide-react';
import { toast } from 'react-hot-toast';
import api, { BASE_DOMAIN } from '../../services/api';
@ -190,7 +190,7 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm transition-opacity">
<div
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
@ -202,7 +202,7 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa
<p className="text-xl font-bold">تصویر را اینجا رها کنید تا آپلود شود</p>
</div>
)}
{/* Header */}
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/50">
<div className="flex items-center gap-3">
@ -234,14 +234,14 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa
<Clipboard className="w-3.5 h-3.5" />
Ctrl+V برای چسباندن تصویر
</span>
<input
type="file"
ref={fileInputRef}
onChange={handleFileUpload}
accept="image/*"
className="hidden"
<input
type="file"
ref={fileInputRef}
onChange={handleFileUpload}
accept="image/*"
className="hidden"
/>
<button
<button
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
className="bg-purple-600 hover:bg-purple-700 disabled:opacity-70 text-white px-4 py-2 rounded-xl flex items-center gap-2 text-sm font-bold transition-colors"
@ -273,7 +273,7 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa
const imgUrl = media.url.startsWith('http') ? media.url : `${BASE_DOMAIN}${media.url}`;
const isSelected = selectedUrl && imgUrl === selectedUrl;
return (
<div
<div
key={media.id}
onClick={() => {
onSelect(imgUrl);
@ -282,19 +282,19 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa
className={`group relative bg-gray-100 rounded-xl overflow-hidden border-2 cursor-pointer transition-all hover:shadow-lg hover:shadow-purple-100 ${isSelected ? 'border-purple-600 ring-2 ring-purple-300' : 'border-transparent hover:border-purple-500'}`}
>
<div className="aspect-square bg-white p-2 flex items-center justify-center border-b border-gray-100">
<img
src={imgUrl}
alt={media.filename}
<img
src={imgUrl}
alt={media.filename}
className="max-w-full max-h-full object-contain object-center"
/>
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3">
<button
<button
onClick={(e) => { e.stopPropagation(); setDeleteTargetId(media.id); }}
className="w-8 h-8 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transform hover:scale-110 transition-transform"
>
<Trash2 className="w-4 h-4" />
</button>
<button
<button
className="w-8 h-8 rounded-full bg-purple-500 text-white flex items-center justify-center hover:bg-purple-600 transform hover:scale-110 transition-transform"
>
<CheckCircle2 className="w-4 h-4" />
@ -323,7 +323,7 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa
);
})()}
</div>
</div>
<ConfirmModal
@ -336,3 +336,4 @@ export default function MediaSelector({ isOpen, onClose, onSelect, multiple = fa
</div>
);
}

View File

@ -8,6 +8,10 @@ import MediaSelector from '../components/ui/MediaSelector';
import ConfirmModal from '../components/ui/ConfirmModal';
export interface BlogPost {
author?: {
firstName?: string;
lastName?: string;
};
id: string;
title: string;
slug: string;
@ -130,7 +134,7 @@ export default function Blogs() {
</h2>
<p className="text-gray-500 font-medium mt-1">مدیریت محتوای آموزشی و مقالات علمی سایت</p>
</div>
<button
<button
onClick={() => openModal()}
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
>
@ -142,9 +146,9 @@ export default function Blogs() {
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="جستجو در مقالات..."
<input
type="text"
placeholder="جستجو در مقالات..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
@ -185,9 +189,9 @@ export default function Blogs() {
<td className="py-4 px-6 text-gray-500 text-sm">{(blog.author?.firstName && blog.author?.lastName) ? `${blog.author.firstName} ${blog.author.lastName}` : 'نامشخص'}</td>
<td className="py-4 px-6">
{blog.isPublished ? (
<span className="flex items-center gap-1 text-green-600 text-sm font-bold"><CheckCircle2 className="w-4 h-4"/>منتشر شده</span>
<span className="flex items-center gap-1 text-green-600 text-sm font-bold"><CheckCircle2 className="w-4 h-4" />منتشر شده</span>
) : (
<span className="flex items-center gap-1 text-orange-600 text-sm font-bold"><XCircle className="w-4 h-4"/>پیشنویس</span>
<span className="flex items-center gap-1 text-orange-600 text-sm font-bold"><XCircle className="w-4 h-4" />پیشنویس</span>
)}
</td>
<td className="py-4 px-6">
@ -220,23 +224,23 @@ export default function Blogs() {
<XCircle className="w-6 h-6" />
</button>
</div>
<div className="p-6 overflow-y-auto flex-1">
<form id="blogForm" onSubmit={handleSave} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">عنوان مقاله *</label>
<input required type="text" value={formData.title} onChange={(e) => setFormData({...formData, title: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
<input required type="text" value={formData.title} onChange={(e) => setFormData({ ...formData, title: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">اسلاگ (Slug) *</label>
<input required type="text" value={formData.slug} dir="ltr" onChange={(e) => setFormData({...formData, slug: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" />
<input required type="text" value={formData.slug} dir="ltr" onChange={(e) => setFormData({ ...formData, slug: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" />
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">محتوای اصلی مقاله *</label>
<textarea required value={formData.content} onChange={(e) => setFormData({...formData, content: e.target.value})} rows={10} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" dir="rtl" placeholder="محتوای مقاله (متن ساده یا HTML)"></textarea>
<textarea required value={formData.content} onChange={(e) => setFormData({ ...formData, content: e.target.value })} rows={10} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm" dir="rtl" placeholder="محتوای مقاله (متن ساده یا HTML)"></textarea>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
@ -253,31 +257,31 @@ export default function Blogs() {
<button type="button" onClick={() => setIsMediaSelectorOpen(true)} className="px-4 py-2 border-2 border-dashed border-gray-300 rounded-xl text-gray-600 font-bold hover:border-purple-500 hover:text-purple-600">انتخاب از گالری</button>
</div>
<label className="flex items-center gap-3 cursor-pointer p-4 border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors">
<input type="checkbox" checked={formData.isPublished} onChange={(e) => setFormData({...formData, isPublished: e.target.checked})} className="w-5 h-5 text-purple-600 rounded focus:ring-purple-500" />
<input type="checkbox" checked={formData.isPublished} onChange={(e) => setFormData({ ...formData, isPublished: e.target.checked })} className="w-5 h-5 text-purple-600 rounded focus:ring-purple-500" />
<span className="font-bold text-gray-700">انتشار فوری در سایت</span>
</label>
</div>
<div className="space-y-4 border border-gray-200 p-4 rounded-xl bg-gray-50/50">
<h4 className="font-bold text-purple-700 text-sm mb-2">تنظیمات سئو (SEO)</h4>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">عنوان متا</label>
<input type="text" value={formData.metaTitle} onChange={(e) => setFormData({...formData, metaTitle: e.target.value})} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
<input type="text" value={formData.metaTitle} onChange={(e) => setFormData({ ...formData, metaTitle: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">کلمات کلیدی</label>
<input type="text" value={formData.keywords} onChange={(e) => setFormData({...formData, keywords: e.target.value})} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
<input type="text" value={formData.keywords} onChange={(e) => setFormData({ ...formData, keywords: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm" />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-gray-700">توضیحات متا</label>
<textarea rows={2} value={formData.metaDescription} onChange={(e) => setFormData({...formData, metaDescription: e.target.value})} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm"></textarea>
<textarea rows={2} value={formData.metaDescription} onChange={(e) => setFormData({ ...formData, metaDescription: e.target.value })} className="w-full px-3 py-2 rounded-lg border border-gray-200 focus:border-purple-500 outline-none text-sm"></textarea>
</div>
</div>
</div>
</form>
</div>
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors">انصراف</button>
<button type="submit" form="blogForm" className="bg-purple-600 hover:bg-purple-700 text-white px-8 py-2.5 rounded-xl font-bold transition-all shadow-md shadow-purple-200">ذخیره مقاله</button>
@ -286,7 +290,7 @@ export default function Blogs() {
</div>
)}
<MediaSelector isOpen={isMediaSelectorOpen} onClose={() => setIsMediaSelectorOpen(false)} onSelect={(url) => setFormData({...formData, imageUrl: url})} />
<MediaSelector isOpen={isMediaSelectorOpen} onClose={() => setIsMediaSelectorOpen(false)} onSelect={(url) => setFormData({ ...formData, imageUrl: url })} />
<ConfirmModal
isOpen={!!deleteTargetId}

View File

@ -9,13 +9,23 @@ import ConfirmModal from '../components/ui/ConfirmModal';
export interface Pet {
id: string;
name: string;
species: string;
species?: string;
type?: string;
breed?: string;
age?: number;
weight?: number;
ownerName?: string;
activityLevel?: string;
imageUrl?: string;
avatarUrl?: string;
createdAt: string;
user?: {
firstName: ReactNode;
lastName: ReactNode;
name?: string;
mobile?: string;
email?: string;
};
ownerName?: string;
createdAt?: string;
}
export default function Pets() {
@ -79,9 +89,9 @@ export default function Pets() {
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="جستجو در نام حیوان..."
<input
type="text"
placeholder="جستجو در نام حیوان..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none transition-all"
@ -127,7 +137,7 @@ export default function Pets() {
<div className="text-gray-500 text-xs font-mono">{pet.user?.mobile || pet.user?.email}</div>
</td>
<td className="py-4 px-6 text-gray-600 text-sm">
{pet.age} ساله، {pet.weight} کیلوگرم<br/>
{pet.age} ساله، {pet.weight} کیلوگرم<br />
<span className="text-xs text-gray-400">تحرک: {pet.activityLevel}</span>
</td>
<td className="py-4 px-6">

View File

@ -11,9 +11,12 @@ export interface Category {
id: string;
nameFa: string;
nameEn?: string;
name?: string;
}
export interface Product {
name?: string;
category?: Category | string;
id: string;
artNo: string;
nameFa: string;
@ -46,7 +49,7 @@ export default function Products() {
const [products, setProducts] = useState<Product[]>([]);
const [categories, setCategories] = useState<Category[]>([]);
const [isLoading, setIsLoading] = useState(true);
// URL and String Helper Functions
const getProductImageUrl = (url: string) => {
if (!url) return '';
@ -254,7 +257,7 @@ export default function Products() {
</h2>
<p className="text-gray-500 font-medium mt-1">افزودن و ویرایش پیشرفته محصولات با تنظیمات سئو</p>
</div>
<button
<button
onClick={() => openModal()}
className="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2.5 rounded-xl flex items-center gap-2 font-bold transition-all shadow-md shadow-purple-200"
>
@ -266,9 +269,9 @@ export default function Products() {
<div className="bg-white p-4 rounded-2xl shadow-sm border border-gray-200 flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="جستجو در نام و کد محصول..."
<input
type="text"
placeholder="جستجو در نام و کد محصول..."
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all"
@ -276,7 +279,7 @@ export default function Products() {
</div>
<div className="relative w-full sm:w-64">
<Filter className="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" />
<select
<select
value={categoryFilter}
onChange={(e) => { setCategoryFilter(e.target.value); setPage(1); }}
className="w-full pl-4 pr-12 py-3 rounded-xl border border-gray-200 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 outline-none transition-all appearance-none"
@ -314,22 +317,22 @@ export default function Products() {
) : (
products.map((product) => (
<tr key={product.id} className="hover:bg-gray-50/50 transition-colors">
<td className="py-3 px-6">
{product.imageUrl && !imageErrors[product.id] ? (
<div className="w-12 h-12 rounded-xl bg-gray-50 border border-gray-200 p-1 flex items-center justify-center overflow-hidden">
<img
src={getProductImageUrl(product.imageUrl)}
alt=""
onError={() => setImageErrors(prev => ({ ...prev, [product.id]: true }))}
className="max-w-full max-h-full object-contain object-center"
/>
</div>
) : (
<div className="w-12 h-12 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
<ImageIcon className="w-5 h-5" />
</div>
)}
</td>
<td className="py-3 px-6">
{product.imageUrl && !imageErrors[product.id] ? (
<div className="w-12 h-12 rounded-xl bg-gray-50 border border-gray-200 p-1 flex items-center justify-center overflow-hidden">
<img
src={getProductImageUrl(product.imageUrl)}
alt=""
onError={() => setImageErrors(prev => ({ ...prev, [product.id]: true }))}
className="max-w-full max-h-full object-contain object-center"
/>
</div>
) : (
<div className="w-12 h-12 rounded-xl bg-gray-100 flex items-center justify-center text-gray-400">
<ImageIcon className="w-5 h-5" />
</div>
)}
</td>
<td className="py-4 px-6 text-gray-500 font-mono text-sm">{product.artNo}</td>
<td className="py-4 px-6">
<div className="flex flex-col gap-0.5">
@ -370,27 +373,27 @@ export default function Products() {
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-sm">
<div className="bg-white rounded-2xl w-full max-w-4xl max-h-[90vh] flex flex-col shadow-2xl animate-in zoom-in duration-200">
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<div className="flex items-center gap-3">
<h3 className="text-xl font-bold text-gray-900">
{editingProduct ? 'ویرایش پیشرفته محصول' : 'افزودن محصول جدید'}
</h3>
{editingProduct && formData.slug && (
<a
href={getStoreProductUrl(formData.slug)}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-purple-50 border border-purple-100 text-purple-600 hover:bg-purple-100 transition-all cursor-pointer"
>
مشاهده آنلاین در سایت 🔗
</a>
)}
</div>
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<X className="w-6 h-6" />
</button>
</div>
<div className="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50/50">
<div className="flex items-center gap-3">
<h3 className="text-xl font-bold text-gray-900">
{editingProduct ? 'ویرایش پیشرفته محصول' : 'افزودن محصول جدید'}
</h3>
{editingProduct && formData.slug && (
<a
href={getStoreProductUrl(formData.slug)}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-purple-50 border border-purple-100 text-purple-600 hover:bg-purple-100 transition-all cursor-pointer"
>
مشاهده آنلاین در سایت 🔗
</a>
)}
</div>
<button onClick={() => setIsModalOpen(false)} className="text-gray-400 hover:text-red-500 transition-colors">
<X className="w-6 h-6" />
</button>
</div>
<div className="flex border-b border-gray-100 px-6 pt-2 gap-4">
{[
{ id: 'general', label: 'اطلاعات پایه' },
@ -411,7 +414,7 @@ export default function Products() {
<div className="p-6 overflow-y-auto h-[550px] min-h-[550px] max-h-[550px] bg-gray-50/30">
<form id="productForm" onSubmit={handleSave} className="space-y-6">
{/* General Tab */}
<div className={activeTab === 'general' ? 'block space-y-4' : 'hidden'}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@ -425,7 +428,7 @@ export default function Products() {
</div>
</div>
</label>
<input required type="text" value={formData.artNo} onChange={(e) => setFormData({...formData, artNo: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono" dir="ltr" />
<input required type="text" value={formData.artNo} onChange={(e) => setFormData({ ...formData, artNo: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono" dir="ltr" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
@ -437,7 +440,7 @@ export default function Products() {
</div>
</div>
</label>
<input required type="text" value={formData.nameFa} onChange={(e) => setFormData({...formData, nameFa: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" dir="rtl" placeholder="مثال: ژل دندان میکروسیلور" />
<input required type="text" value={formData.nameFa} onChange={(e) => setFormData({ ...formData, nameFa: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" dir="rtl" placeholder="مثال: ژل دندان میکروسیلور" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
@ -449,7 +452,7 @@ export default function Products() {
</div>
</div>
</label>
<input required type="text" value={formData.nameEn} onChange={(e) => setFormData({...formData, nameEn: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono" dir="ltr" placeholder="e.g. Canina Mikrosilber Zahngel - 50ml" />
<input required type="text" value={formData.nameEn} onChange={(e) => setFormData({ ...formData, nameEn: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono" dir="ltr" placeholder="e.g. Canina Mikrosilber Zahngel - 50ml" />
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
@ -461,7 +464,7 @@ export default function Products() {
</div>
</div>
</label>
<input type="text" value={formData.scientificTagline} onChange={(e) => setFormData({...formData, scientificTagline: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
<input type="text" value={formData.scientificTagline} onChange={(e) => setFormData({ ...formData, scientificTagline: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
@ -473,7 +476,7 @@ export default function Products() {
</div>
</div>
</label>
<select required value={formData.categoryId} onChange={(e) => setFormData({...formData, categoryId: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none bg-white">
<select required value={formData.categoryId} onChange={(e) => setFormData({ ...formData, categoryId: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none bg-white">
<option value="">انتخاب کنید</option>
{categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
@ -488,7 +491,7 @@ export default function Products() {
</div>
</div>
</label>
<select value={formData.suitableFor} onChange={(e) => setFormData({...formData, suitableFor: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none bg-white">
<select value={formData.suitableFor} onChange={(e) => setFormData({ ...formData, suitableFor: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none bg-white">
<option value="سگ">سگ</option>
<option value="گربه">گربه</option>
<option value="سگ و گربه">سگ و گربه</option>
@ -504,7 +507,7 @@ export default function Products() {
</div>
</div>
</label>
<textarea rows={2} value={formData.shortDescription} onChange={(e) => setFormData({...formData, shortDescription: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
<textarea rows={2} value={formData.shortDescription} onChange={(e) => setFormData({ ...formData, shortDescription: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
@ -516,9 +519,9 @@ export default function Products() {
</div>
</div>
</label>
<textarea rows={5} value={formData.description} onChange={(e) => setFormData({...formData, description: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
<textarea rows={5} value={formData.description} onChange={(e) => setFormData({ ...formData, description: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
</div>
<div className="space-y-2 md:col-span-2">
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>منطق دوز مصرفی (Dosage Logic)</span>
<div className="group relative inline-block">
@ -528,9 +531,9 @@ export default function Products() {
</div>
</div>
</label>
<textarea rows={2} value={formData.dosageLogic} onChange={(e) => setFormData({...formData, dosageLogic: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
</div>
<div className="space-y-2 md:col-span-2">
<textarea rows={2} value={formData.dosageLogic} onChange={(e) => setFormData({ ...formData, dosageLogic: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"></textarea>
</div>
<div className="space-y-2 md:col-span-2">
<label className="text-sm font-bold text-gray-700 flex items-center gap-1.5">
<span>علائم درمانی مرتبط (Symptom Tags)</span>
<div className="group relative inline-block">
@ -540,46 +543,46 @@ export default function Products() {
</div>
</div>
</label>
<div className="flex flex-wrap gap-2 p-3 bg-white border border-gray-200 rounded-xl min-h-[50px] items-center focus-within:border-purple-500 focus-within:ring-2 focus-within:ring-purple-200 transition-all">
{formData.symptoms && formData.symptoms.map((symptom, idx) => (
<span key={idx} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-purple-50 border border-purple-200 text-purple-700">
{symptom}
<button
type="button"
onClick={() => {
const newSymptoms = formData.symptoms.filter((_, i) => i !== idx);
setFormData({ ...formData, symptoms: newSymptoms });
}}
className="text-purple-400 hover:text-purple-600 focus:outline-none transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</span>
))}
<input
type="text"
placeholder="تایپ و اینتر کنید..."
className="flex-1 bg-transparent border-none outline-none text-sm min-w-[150px] text-right font-vazir"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
const input = e.currentTarget;
const value = input.value.trim();
if (value && !formData.symptoms.includes(value)) {
setFormData({
...formData,
symptoms: [...(formData.symptoms || []), value]
});
input.value = '';
}
}
}}
/>
</div>
<p className="text-[11px] text-gray-400 font-medium">پس از نوشتن هر علامت درمانی (مثلاً «درد مفاصل»)، کلید Enter را فشار دهید.</p>
</div>
</div>
</div>
<div className="flex flex-wrap gap-2 p-3 bg-white border border-gray-200 rounded-xl min-h-[50px] items-center focus-within:border-purple-500 focus-within:ring-2 focus-within:ring-purple-200 transition-all">
{formData.symptoms && formData.symptoms.map((symptom, idx) => (
<span key={idx} className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-purple-50 border border-purple-200 text-purple-700">
{symptom}
<button
type="button"
onClick={() => {
const newSymptoms = formData.symptoms.filter((_, i) => i !== idx);
setFormData({ ...formData, symptoms: newSymptoms });
}}
className="text-purple-400 hover:text-purple-600 focus:outline-none transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</span>
))}
<input
type="text"
placeholder="تایپ و اینتر کنید..."
className="flex-1 bg-transparent border-none outline-none text-sm min-w-[150px] text-right font-vazir"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
const input = e.currentTarget;
const value = input.value.trim();
if (value && !formData.symptoms.includes(value)) {
setFormData({
...formData,
symptoms: [...(formData.symptoms || []), value]
});
input.value = '';
}
}
}}
/>
</div>
<p className="text-[11px] text-gray-400 font-medium">پس از نوشتن هر علامت درمانی (مثلاً «درد مفاصل»)، کلید Enter را فشار دهید.</p>
</div>
</div>
</div>
{/* Pricing Tab */}
<div className={activeTab === 'pricing' ? 'block space-y-4' : 'hidden'}>
@ -608,15 +611,15 @@ export default function Products() {
<span>قیمت اختصاصی B2B (تومان)</span>
<span className="text-[10px] font-normal text-gray-400">(اختیاری)</span>
</label>
<input type="number" min="0" placeholder="در صورت عدم ورود، درصد کلی اعمال می‌شود" value={formData.wholesalePrice} onChange={(e) => setFormData({...formData, wholesalePrice: e.target.value ? Number(e.target.value) : ''})} className="w-full px-4 py-3 rounded-xl border border-purple-200 focus:border-purple-600 outline-none font-bold bg-purple-50/20" dir="ltr" />
<input type="number" min="0" placeholder="در صورت عدم ورود، درصد کلی اعمال می‌شود" value={formData.wholesalePrice} onChange={(e) => setFormData({ ...formData, wholesalePrice: e.target.value ? Number(e.target.value) : '' })} className="w-full px-4 py-3 rounded-xl border border-purple-200 focus:border-purple-600 outline-none font-bold bg-purple-50/20" dir="ltr" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">موجودی *</label>
<input required type="number" min="0" value={formData.packageSize} onChange={(e) => setFormData({...formData, packageSize: Number(e.target.value)})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" dir="ltr" />
<input required type="number" min="0" value={formData.packageSize} onChange={(e) => setFormData({ ...formData, packageSize: Number(e.target.value) })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" dir="ltr" />
</div>
<div className="space-y-2">
<label className="text-sm font-bold text-gray-700">واحد (Unit) *</label>
<input required type="text" placeholder="عدد، گرم، میلی‌لیتر" value={formData.unit} onChange={(e) => setFormData({...formData, unit: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
<input required type="text" placeholder="عدد، گرم، میلی‌لیتر" value={formData.unit} onChange={(e) => setFormData({ ...formData, unit: e.target.value })} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
</div>
</div>
</div>
@ -644,13 +647,13 @@ export default function Products() {
</div>
</div>
</label>
<input
type="text"
value={formData.slug}
onChange={(e) => setFormData({...formData, slug: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
dir="ltr"
placeholder={derivedSlug}
<input
type="text"
value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
dir="ltr"
placeholder={derivedSlug}
/>
</div>
@ -664,11 +667,11 @@ export default function Products() {
</div>
</div>
</label>
<input
type="text"
value={formData.metaTitle}
onChange={(e) => setFormData({...formData, metaTitle: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
<input
type="text"
value={formData.metaTitle}
onChange={(e) => setFormData({ ...formData, metaTitle: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
placeholder={derivedMetaTitle}
/>
</div>
@ -683,10 +686,10 @@ export default function Products() {
</div>
</div>
</label>
<textarea
rows={3}
value={formData.metaDescription}
onChange={(e) => setFormData({...formData, metaDescription: e.target.value})}
<textarea
rows={3}
value={formData.metaDescription}
onChange={(e) => setFormData({ ...formData, metaDescription: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
placeholder={derivedMetaDescription}
></textarea>
@ -702,11 +705,11 @@ export default function Products() {
</div>
</div>
</label>
<input
type="text"
value={formData.keywords}
onChange={(e) => setFormData({...formData, keywords: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
<input
type="text"
value={formData.keywords}
onChange={(e) => setFormData({ ...formData, keywords: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none"
placeholder="مثال: مکمل سگ, تقویت مفاصل, کنهیدروکس"
/>
</div>
@ -721,12 +724,12 @@ export default function Products() {
</div>
</div>
</label>
<input
type="text"
value={formData.canonicalUrl}
onChange={(e) => setFormData({...formData, canonicalUrl: e.target.value})}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm"
dir="ltr"
<input
type="text"
value={formData.canonicalUrl}
onChange={(e) => setFormData({ ...formData, canonicalUrl: e.target.value })}
className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm"
dir="ltr"
placeholder={derivedCanonicalUrl}
/>
</div>
@ -735,7 +738,7 @@ export default function Products() {
{/* Left Column: Sticky Google SERP Preview */}
<div className="lg:col-span-5 lg:sticky lg:top-4 bg-gray-100 border border-gray-200 rounded-2xl p-5 space-y-3">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest block">پیشنمایش نتایج در گوگل (Google SERP Preview)</span>
<div className="bg-white rounded-xl p-4 border border-gray-100 shadow-sm text-right font-sans" dir="rtl">
{/* Domain / Breadcrumb */}
<div className="flex items-center gap-1 text-xs text-[#202124] mb-1">
@ -745,12 +748,12 @@ export default function Products() {
<span className="text-gray-400"></span>
<span className="text-gray-500 max-w-[150px] truncate">{derivedSlug}</span>
</div>
{/* Title */}
<h4 className="text-[19px] text-[#1a0dab] hover:underline cursor-pointer leading-tight font-medium font-sans mb-1 select-none">
{derivedMetaTitle}
</h4>
{/* Description */}
<p className="text-sm text-[#4d5156] leading-relaxed font-sans select-none break-words">
{derivedMetaDescription}
@ -775,19 +778,19 @@ export default function Products() {
<span className="text-[11px] text-gray-400 font-normal">امکان چسباندن (Ctrl+V) مستقیم آدرس اینترنتی تصویر</span>
</label>
<div className="flex gap-2">
<input
type="text"
value={formData.imageUrl}
<input
type="text"
value={formData.imageUrl}
onChange={(e) => {
setFormData({...formData, imageUrl: e.target.value});
setFormData({ ...formData, imageUrl: e.target.value });
setMediaImageError(false);
}}
}}
placeholder="https://example.com/images/product.png یا /uploads/photo.jpg"
className="flex-1 px-4 py-2.5 rounded-xl border border-gray-200 focus:border-purple-500 outline-none font-mono text-sm"
dir="ltr"
/>
<button
type="button"
<button
type="button"
onClick={() => setIsMediaSelectorOpen(true)}
className="bg-purple-100 text-purple-700 hover:bg-purple-200 px-4 py-2.5 rounded-xl text-xs font-bold transition-colors whitespace-nowrap"
>
@ -801,15 +804,15 @@ export default function Products() {
{formData.imageUrl && !mediaImageError ? (
<div className="w-full flex flex-col items-center">
<div className="relative group">
<img
src={getProductImageUrl(formData.imageUrl)}
alt="پیش‌نمایش تصویر محصول"
<img
src={getProductImageUrl(formData.imageUrl)}
alt="پیش‌نمایش تصویر محصول"
onError={() => setMediaImageError(true)}
className="max-w-xs h-48 object-contain rounded-xl shadow-md border border-gray-100"
className="max-w-xs h-48 object-contain rounded-xl shadow-md border border-gray-100"
/>
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-xl flex items-center justify-center gap-3">
<button type="button" onClick={() => setIsMediaSelectorOpen(true)} className="bg-white text-gray-800 px-4 py-2 rounded-lg font-bold text-sm">انتخاب از گالری</button>
<button type="button" onClick={() => setFormData({...formData, imageUrl: ''})} className="bg-red-500 text-white px-4 py-2 rounded-lg font-bold text-sm hover:bg-red-600">حذف عکس</button>
<button type="button" onClick={() => setFormData({ ...formData, imageUrl: '' })} className="bg-red-500 text-white px-4 py-2 rounded-lg font-bold text-sm hover:bg-red-600">حذف عکس</button>
</div>
</div>
<div className="mt-4 text-center" dir="ltr">
@ -839,7 +842,7 @@ export default function Products() {
{/* Multimedia Attachments (TASK-2.3 & TASK-2.4) */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-4">
<h4 className="font-bold text-gray-800 text-sm border-b pb-2">پیوستهای مالتیمدیا و مستندات علمی</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-1.5">
<label className="text-xs font-bold text-gray-700 block">لینک فصلی پادکست (Podcast URL)</label>
@ -880,7 +883,7 @@ export default function Products() {
</form>
</div>
<div className="p-4 border-t border-gray-100 bg-gray-50 flex justify-end gap-3 rounded-b-2xl">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-gray-600 hover:bg-gray-200 transition-colors">
انصراف
@ -894,13 +897,13 @@ export default function Products() {
)}
{/* Media Selector Modal */}
<MediaSelector
isOpen={isMediaSelectorOpen}
onClose={() => setIsMediaSelectorOpen(false)}
<MediaSelector
isOpen={isMediaSelectorOpen}
onClose={() => setIsMediaSelectorOpen(false)}
onSelect={(url) => {
setFormData({...formData, imageUrl: url});
setFormData({ ...formData, imageUrl: url });
setMediaImageError(false);
}}
}}
selectedUrl={formData.imageUrl.startsWith('http') ? formData.imageUrl : formData.imageUrl ? `${BASE_DOMAIN}${formData.imageUrl}` : undefined}
/>

View File

@ -6,6 +6,7 @@ import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination';
export interface UserRecord {
[x: string]: string;
id: string;
firstName?: string;
lastName?: string;
@ -18,7 +19,7 @@ export interface UserRecord {
export default function Users() {
const [users, setUsers] = useState<UserRecord[]>([]);
const [isLoading, setIsLoading] = useState(true);
// Queries
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
@ -85,9 +86,9 @@ export default function Users() {
</h2>
<p className="text-gray-500 font-medium mt-1">مشاهده و تایید حسابهای کاربری و کلینیکها</p>
</div>
<div className="flex flex-col sm:flex-row items-center gap-3 w-full sm:w-auto">
<select
<select
className="bg-white border border-gray-200 text-gray-700 px-4 py-2 rounded-xl flex items-center gap-2 font-bold hover:bg-gray-50 transition-all outline-none w-full sm:w-auto"
value={role}
onChange={(e) => { setRole(e.target.value); setPage(1); }}
@ -99,9 +100,9 @@ export default function Users() {
</select>
<div className="relative w-full sm:w-64">
<Search className="w-5 h-5 text-gray-400 absolute right-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="جستجو کاربر (نام، ایمیل، موبایل)..."
<input
type="text"
placeholder="جستجو کاربر (نام، ایمیل، موبایل)..."
className="pl-4 pr-10 py-2 border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-purple-500 w-full font-medium"
dir="rtl"
value={search}
@ -146,7 +147,7 @@ export default function Users() {
<td className="py-4 px-6 text-gray-500" dir="ltr">{user.email}</td>
<td className="py-4 px-6">
{isEditing ? (
<select
<select
className="border border-purple-300 rounded p-1 text-xs font-bold"
value={editRole}
onChange={(e) => setEditRole(e.target.value)}
@ -157,13 +158,12 @@ export default function Users() {
<option value="ADMIN">ادمین</option>
</select>
) : (
<span className={`px-3 py-1 rounded-full text-xs font-bold ${
user.role === 'User_Wholesale' ? 'bg-amber-100 text-amber-800' :
user.role?.includes('B2B') ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
}`}>
<span className={`px-3 py-1 rounded-full text-xs font-bold ${user.role === 'User_Wholesale' ? 'bg-amber-100 text-amber-800' :
user.role?.includes('B2B') ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
}`}>
{user.role === 'User_Wholesale' ? 'خریدار عمده' :
user.role?.includes('B2B') ? 'همکار (B2B)' :
(user.role === 'ADMIN' ? 'مدیر سیستم' : 'مشتری عادی')}
user.role?.includes('B2B') ? 'همکار (B2B)' :
(user.role === 'ADMIN' ? 'مدیر سیستم' : 'مشتری عادی')}
</span>
)}
</td>

View File

@ -433,3 +433,8 @@ export default function Videos() {
</div>
);
}
function fetchVideosList() {
throw new Error('Function not implemented.');
}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { toast } from 'react-hot-toast';
import api from '../services/api';
import ConfirmModal from '../components/ui/ConfirmModal';
@ -99,9 +99,8 @@ export default function WholesaleApplications() {
<div className="text-xs text-gray-400 dir-ltr text-right">{u.email || u.phoneNumber}</div>
</td>
<td className="px-6 py-4">
<span className={`inline-flex px-3 py-1 rounded-full text-xs font-bold ${
u.role === 'User_Wholesale' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'
}`}>
<span className={`inline-flex px-3 py-1 rounded-full text-xs font-bold ${u.role === 'User_Wholesale' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'
}`}>
{u.role === 'User_Wholesale' ? 'خریدار عمده تاییدشده' : 'در انتظار بررسی B2B'}
</span>
</td>

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { Search, Plus, Edit2, Trash2, BookOpen, XCircle } from 'lucide-react';
import { toast } from 'react-hot-toast';
import api from '../services/api';

View File

@ -103,7 +103,7 @@ export default function ClientLayout({ children }: { children: React.ReactNode }
onLogin={() => {
setLoginModalOpen(false);
}}
petName={advisorData?.name}
petName={advisorData?.name as string | undefined}
isAdvisorContext={!!advisorData}
/>
<Toaster position="top-center" expand={true} richColors closeButton />

View File

@ -117,7 +117,7 @@ export default function PetProfile({ initialView, advisorNeed }: {
const recommendedProducts = useMemo(() => {
if (!activePet || products.length === 0) return [];
let picks: { product: Product, reason?: string }[] = [];
const picks: { product: Product, reason?: string }[] = [];
// Priority 1: Match activePet.medicalConditions directly against product.symptoms
if (activePet.medicalConditions && activePet.medicalConditions.length > 0) {

View File

@ -1,11 +1,11 @@
"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import { motion } 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, MessageSquare, Send, Stethoscope, FileText } 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 { toPersian, toEnglishDigits, cn } from "../lib/utils";
import { toast } from "sonner";
import OrderDetailsModal from "./OrderDetailsModal";
import AddressModal from "./AddressModal";
@ -16,7 +16,7 @@ import { useRouter } from 'next/navigation';
export default function UserDashboard() {
const router = useRouter();
const { profile, isLoggedIn, logout, addAddress, updateAddress, deleteAddress, setDefaultAddress, topUpWallet } = useUserStore();
const { profile, isLoggedIn, logout, updateProfile, addAddress, updateAddress, deleteAddress, setDefaultAddress, topUpWallet } = useUserStore();
React.useEffect(() => {
if (!isLoggedIn && typeof window !== "undefined") {
@ -29,12 +29,19 @@ export default function UserDashboard() {
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") {
setIsLoadingOrders(true);
useUserStore.getState().fetchProfile().finally(() => setIsLoadingOrders(false));
let isSubscribed = true;
Promise.resolve().then(() => {
if (isSubscribed) setIsLoadingOrders(true);
});
useUserStore.getState().fetchProfile().finally(() => {
if (isSubscribed) setIsLoadingOrders(false);
});
return () => {
isSubscribed = false;
};
}
}, [activeTab]);
const [selectedOrder, setSelectedOrder] = useState<any>(null);
@ -51,21 +58,29 @@ export default function UserDashboard() {
// Profile State
const [isEditing, setIsEditing] = useState(false);
const [isSavingProfile, setIsSavingProfile] = useState(false);
const [profileForm, setProfileForm] = useState({
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 || ""
let isSubscribed = true;
Promise.resolve().then(() => {
if (isSubscribed) {
setProfileForm({
firstName: profile.firstName || "",
lastName: profile.lastName || "",
email: profile.email || "",
mobile: profile.mobile || ""
});
}
});
return () => {
isSubscribed = false;
};
}, [profile]);
if (!isLoggedIn) {
@ -74,17 +89,10 @@ export default function UserDashboard() {
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({
const cleanPhone = toEnglishDigits(profileForm.mobile);
await updateProfile({
firstName: profileForm.firstName,
lastName: profileForm.lastName,
email: profileForm.email,
@ -92,8 +100,8 @@ export default function UserDashboard() {
});
setIsEditing(false);
toast.success("اطلاعات کاربری با موفقیت ویرایش شد");
} catch (err: any) {
toast.error(err.message || "خطا در ویرایش اطلاعات");
} catch {
toast.error("خطا در ویرایش اطلاعات");
} finally {
setIsSavingProfile(false);
}
@ -111,7 +119,7 @@ export default function UserDashboard() {
} else {
await addAddress(addr);
}
} catch (err) {
} catch {
toast.error("خطا در ثبت آدرس");
}
setEditingAddress(null);
@ -127,7 +135,7 @@ export default function UserDashboard() {
try {
await deleteAddress(addressToDelete.id);
toast.success("آدرس با موفقیت حذف شد");
} catch (err) {
} catch {
toast.error("خطا در حذف آدرس");
}
setAddressToDelete(null);
@ -370,7 +378,7 @@ export default function UserDashboard() {
<div className="text-sm font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors italic">سفارش {toPersian(order.trackingNumber || order.id?.substring(0, 8))}</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 || (order as any).createdAt).toLocaleDateString("fa-IR"))} - ساعت {toPersian(new Date(order.date || (order as any).createdAt).toLocaleTimeString("fa-IR", { hour: '2-digit', minute: '2-digit' }))}
{toPersian(new Date(order.date || (order as unknown as Record<string, string>).createdAt || 0).toLocaleDateString("fa-IR"))} - ساعت {toPersian(new Date(order.date || (order as unknown as Record<string, string>).createdAt || 0).toLocaleTimeString("fa-IR", { hour: '2-digit', minute: '2-digit' }))}
</div>
</div>
</div>
@ -477,7 +485,7 @@ export default function UserDashboard() {
try {
await setDefaultAddress(addr.id);
toast.success("آدرس پیش‌فرض با موفقیت تغییر کرد");
} catch (err) {
} catch {
toast.error("خطا در تغییر آدرس پیش‌فرض");
}
}}

View File

@ -1,13 +1,24 @@
"use client";
import React, { useState } from "react";
import { Play, PlayCircle, Star, ShieldCheck, X } from "lucide-react";
import { Play, Star, ShieldCheck, X } from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import SafeImage from "./SafeImage";
import { useSettingsStore } from "../lib/store/settingsStore";
import { videoService, Video } from "../lib/services/videoService";
export interface TestimonialItem {
id?: string;
clinicName?: string;
vetName?: string;
imageUrl?: string;
quote?: string;
title?: string;
thumbnail?: string;
videoUrl?: string;
description?: string;
}
const FALLBACK_VIDEOS = [
{
id: "v1",
@ -38,11 +49,10 @@ const FALLBACK_VIDEOS = [
}
];
export default function VetGallery({ testimonials = [] }: { testimonials?: any[] }) {
const router = useRouter();
export default function VetGallery({ testimonials = [] }: { testimonials?: TestimonialItem[] }) {
const getText = useSettingsStore(state => state.getText);
const [apiVideos, setApiVideos] = useState<Video[]>([]);
const [selectedVideo, setSelectedVideo] = useState<any>(null);
const [selectedVideo, setSelectedVideo] = useState<any | null>(null);
React.useEffect(() => {
videoService.getVideos({ featured: true, limit: 3 }).then((data) => {
@ -55,13 +65,13 @@ export default function VetGallery({ testimonials = [] }: { testimonials?: any[]
const displayItems = testimonials.length > 0
? testimonials.map((t, idx) => ({
id: t.id || `t-${idx}`,
title: t.clinicName ? `${t.vetName}${t.clinicName}` : t.vetName,
doctor: t.vetName,
title: t.clinicName ? `${t.vetName}${t.clinicName}` : (t.vetName || ''),
doctor: t.vetName || '',
duration: "۰۲:۰۰",
thumbnail: t.imageUrl || "https://images.unsplash.com/photo-1576091160550-217359f42f8c?auto=format&fit=crop&q=80&w=400",
videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
description: t.quote,
quote: t.quote
description: t.quote || '',
quote: t.quote || ''
}))
: (apiVideos.length > 0 ? apiVideos : FALLBACK_VIDEOS);
@ -102,7 +112,7 @@ export default function VetGallery({ testimonials = [] }: { testimonials?: any[]
<div className="absolute inset-0 bg-medical-gray-900/20 group-hover:bg-transparent transition-colors z-10" />
<SafeImage
src={video.thumbnail || video.imageUrl || '/images/vets/vet1.webp'}
alt={video.title || video.vetName}
alt={video.title || video.vetName || 'ویدیو دامپزشک'}
className="w-full h-full"
imgClassName="object-cover grayscale-[30%] group-hover:grayscale-0 transition-all duration-700"
/>
@ -117,7 +127,7 @@ export default function VetGallery({ testimonials = [] }: { testimonials?: any[]
</div>
</div>
<h4 className="text-lg font-black text-medical-gray-900 mb-2 group-hover:text-canina-blue transition-colors leading-tight font-vazir">
{video.title || video.quote?.substring(0, 40) + '...'}
{video.title || (video.quote ? video.quote.substring(0, 40) + '...' : '')}
</h4>
<div className="flex items-center gap-2 text-medical-gray-500 font-bold text-sm font-vazir">
<ShieldCheck className="w-4 h-4 text-canina-blue" />

View File

@ -1,7 +1,7 @@
"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import { ChevronRight, PlayCircle, Star, ShieldCheck, X, Search, Clock, User } from "lucide-react";
import { ChevronRight, PlayCircle, ShieldCheck, X, Search, Clock, User } from "lucide-react";
import SafeImage from "./SafeImage";

View File

@ -44,7 +44,7 @@ describe('CartDrawer', () => {
coupon: null,
applyCoupon: vi.fn(),
addItem: vi.fn(),
} as any);
} as unknown as ReturnType<typeof useCartStore>);
render(<CartDrawer isOpen={true} onClose={vi.fn()} onCheckout={vi.fn()} />);
@ -64,7 +64,7 @@ describe('CartDrawer', () => {
coupon: null,
applyCoupon: vi.fn(),
addItem: vi.fn(),
} as any);
} as unknown as ReturnType<typeof useCartStore>);
render(<CartDrawer isOpen={true} onClose={vi.fn()} onCheckout={vi.fn()} />);
@ -86,7 +86,7 @@ describe('CartDrawer', () => {
coupon: null,
applyCoupon: vi.fn(),
addItem: vi.fn(),
} as any);
} as unknown as ReturnType<typeof useCartStore>);
render(<CartDrawer isOpen={true} onClose={vi.fn()} onCheckout={vi.fn()} />);

View File

@ -37,7 +37,7 @@ describe('FeaturedProducts', () => {
vi.clearAllMocks();
vi.mocked(usePetStore).mockReturnValue({
getActivePet: () => null,
} as any);
} as unknown as ReturnType<typeof usePetStore>);
});
it('renders loading skeletons initially', () => {
@ -48,7 +48,7 @@ describe('FeaturedProducts', () => {
});
it('renders products once loaded', async () => {
vi.mocked(productService.getFeaturedProducts).mockResolvedValue(mockProducts as any);
vi.mocked(productService.getFeaturedProducts).mockResolvedValue(mockProducts as unknown as ReturnType<typeof productService.getFeaturedProducts>);
render(<FeaturedProducts onProductClick={vi.fn()} onShopNavigate={vi.fn()} />);
await waitFor(() => {
@ -59,7 +59,7 @@ describe('FeaturedProducts', () => {
it('calls onProductClick when product card is clicked', async () => {
const handleProductClick = vi.fn();
vi.mocked(productService.getFeaturedProducts).mockResolvedValue(mockProducts as any);
vi.mocked(productService.getFeaturedProducts).mockResolvedValue(mockProducts as unknown as ReturnType<typeof productService.getFeaturedProducts>);
render(<FeaturedProducts onProductClick={handleProductClick} onShopNavigate={vi.fn()} />);
await waitFor(() => {
@ -72,7 +72,7 @@ describe('FeaturedProducts', () => {
it('calls onShopNavigate when navigation link is clicked', async () => {
const handleShopNavigate = vi.fn();
vi.mocked(productService.getFeaturedProducts).mockResolvedValue(mockProducts as any);
vi.mocked(productService.getFeaturedProducts).mockResolvedValue(mockProducts as unknown as ReturnType<typeof productService.getFeaturedProducts>);
render(<FeaturedProducts onProductClick={vi.fn()} onShopNavigate={handleShopNavigate} />);
const navBtn = screen.getByText('مشاهده تمامی محصولات');

View File

@ -25,21 +25,21 @@ describe('Header', () => {
vi.mocked(useCartStore).mockReturnValue({
getTotalItems: () => 3,
} as any);
} as unknown as ReturnType<typeof useCartStore>);
vi.mocked(usePetStore).mockReturnValue({
pets: [],
activePetId: null,
setActivePet: vi.fn(),
getActivePet: () => null,
} as any);
} as unknown as ReturnType<typeof usePetStore>);
vi.mocked(useUserStore).mockReturnValue({
role: 'User_Guest',
isLoggedIn: false,
logout: vi.fn(),
profile: { firstName: '' },
} as any);
} as unknown as ReturnType<typeof useUserStore>);
});
it('renders brand name and login button when guest', () => {
@ -65,14 +65,14 @@ describe('Header', () => {
isLoggedIn: true,
logout: vi.fn(),
profile: { firstName: 'کوروش' },
} as any);
} as unknown as ReturnType<typeof useUserStore>);
vi.mocked(usePetStore).mockReturnValue({
pets: [{ id: 'pet-1', name: 'ملوس', type: 'گربه' }],
activePetId: 'pet-1',
setActivePet: vi.fn(),
getActivePet: () => ({ id: 'pet-1', name: 'ملوس', type: 'گربه' }),
} as any);
} as unknown as ReturnType<typeof usePetStore>);
render(
<Header

View File

@ -21,7 +21,7 @@ describe('Hero', () => {
return fallback;
});
vi.mocked(useSettingsStore).mockImplementation((selector: any) => selector({ getText: mockGetText }));
vi.mocked(useSettingsStore).mockImplementation((selector: (state: { getText: (key: string, fallback: string) => string }) => unknown) => selector({ getText: mockGetText }) as ReturnType<typeof selector>);
render(<Hero onShopNavigate={vi.fn()} />);
@ -31,8 +31,8 @@ describe('Hero', () => {
});
it('calls onShopNavigate when "مشاهده محصولات" button is clicked', () => {
const mockGetText = vi.fn().mockImplementation((key, fallback) => fallback);
vi.mocked(useSettingsStore).mockImplementation((selector: any) => selector({ getText: mockGetText }));
const mockGetText = vi.fn().mockImplementation((_key, fallback) => fallback);
vi.mocked(useSettingsStore).mockImplementation((selector: (state: { getText: (key: string, fallback: string) => string }) => unknown) => selector({ getText: mockGetText }) as ReturnType<typeof selector>);
const handleShopNavigate = vi.fn();
render(<Hero onShopNavigate={handleShopNavigate} />);

View File

@ -756,7 +756,7 @@ export const PRODUCTS: Product[] = [
contraindications: ["مصرف همزمان ویتامین D2 مجاز نیست"],
calculateDosage: (weight, isPregnant) => {
const rate = isPregnant ? 3 : 2;
let qty = (weight / 10) * rate;
const qty = (weight / 10) * rate;
return { quantity: Math.round(qty), unit: "قرص", description: isPregnant ? "دوز بارداری/شیردهی" : "دوز معمول" };
},
analysis: { "کلسیم": "۲۲٪" },
@ -788,7 +788,7 @@ export const PRODUCTS: Product[] = [
symptoms: ["خستگی مزمن", "رشد نامتناسب", "استرس"],
suitableFor: "هر دو",
calculateDosage: (weight) => {
let qty = Math.max(1, weight / 10);
const qty = Math.max(1, weight / 10);
return { quantity: Math.round(qty), unit: "قرص", description: "روزانه" };
},
analysis: { "ویتامین‌ها": "A تا K3 کامل" },
@ -853,7 +853,7 @@ export const PRODUCTS: Product[] = [
symptoms: ["بینی روشن", "پوشش کدر", "کمبود ید"],
suitableFor: "هر دو",
calculateDosage: (weight) => {
let tablets = (weight / 10) * 2;
const tablets = (weight / 10) * 2;
return { quantity: Math.round(tablets), unit: "قرص", description: "روزانه" };
},
analysis: { "ید": "غنی", "لیزین": "+" },
@ -1024,7 +1024,7 @@ export const PRODUCTS: Product[] = [
suitableFor: "گربه",
onSetOfAction: "هفته دوم",
calculateDosage: (weight) => {
let tsp = weight > 6 ? 1 : 0.5;
const tsp = weight > 6 ? 1 : 0.5;
return { quantity: tsp, unit: "قاشق چایخوری", description: "روزانه با غذا مخلوط شود" };
},
analysis: { "تورین": "۷۰۰ گرم/kg" },

View File

@ -1,13 +1,11 @@
import { useState, useEffect } from 'react';
export function useNetworkStatus() {
const [isOnline, setIsOnline] = useState(true);
const [isOnline, setIsOnline] = useState(() =>
typeof navigator !== 'undefined' ? navigator.onLine : true
);
useEffect(() => {
if (typeof window !== 'undefined' && typeof navigator !== 'undefined') {
setIsOnline(navigator.onLine);
}
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);

View File

@ -1,5 +1,6 @@
import axios from 'axios';
import { toast } from 'sonner';
import { useUserStore } from '../store/userStore';
const baseURL = process.env.NEXT_PUBLIC_API_URL || (process.env.NODE_ENV === 'development' ? 'http://localhost:4001/api' : '/api');
@ -8,7 +9,7 @@ export interface ApiErrorPayload {
statusCode: number;
message: string;
code: string;
details?: Array<{ field: string; message: string }> | Record<string, any>;
details?: Array<{ field: string; message: string }> | Record<string, unknown>;
timestamp?: string;
path?: string;
}
@ -50,7 +51,6 @@ api.interceptors.response.use(
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
try {
const { useUserStore } = require('../store/userStore');
useUserStore.getState().logout();
} catch {
// Ignore if store not ready

View File

@ -11,8 +11,8 @@ export interface User {
charityDonationTotal: number;
createdAt: string;
updatedAt: string;
pets?: any[];
orders?: any[];
pets?: unknown[];
orders?: unknown[];
}
export interface AuthResponse {
@ -23,6 +23,15 @@ export interface AuthResponse {
};
}
interface ApiErr {
response?: {
status?: number;
data?: {
message?: string | string[];
};
};
}
export class AuthService {
private static instance: AuthService;
@ -42,8 +51,9 @@ export class AuthService {
try {
const response = await api.post('/auth/send-otp', { phoneNumber });
return response.data;
} catch (error: any) {
const message = error.response?.data?.message || 'خطا در ارسال کد تایید';
} catch (error) {
const err = error as ApiErr;
const message = err.response?.data?.message || 'خطا در ارسال کد تایید';
throw new Error(Array.isArray(message) ? message[0] : message);
}
}
@ -59,8 +69,9 @@ export class AuthService {
localStorage.setItem('accessToken', data.accessToken);
}
return response.data;
} catch (error: any) {
const message = error.response?.data?.message || 'کد تایید نامعتبر است';
} catch (error) {
const err = error as ApiErr;
const message = err.response?.data?.message || 'کد تایید نامعتبر است';
throw new Error(Array.isArray(message) ? message[0] : message);
}
}
@ -68,7 +79,7 @@ export class AuthService {
/**
* Register with email/mobile and password
*/
public async register(data: any): Promise<AuthResponse> {
public async register(data: Record<string, unknown>): Promise<AuthResponse> {
try {
const response = await api.post('/auth/register', data);
const { data: resData } = response.data;
@ -76,8 +87,9 @@ export class AuthService {
localStorage.setItem('accessToken', resData.accessToken);
}
return response.data;
} catch (error: any) {
const message = error.response?.data?.message || 'خطا در ثبت‌نام';
} catch (error) {
const err = error as ApiErr;
const message = err.response?.data?.message || 'خطا در ثبت‌نام';
throw new Error(Array.isArray(message) ? message[0] : message);
}
}
@ -93,8 +105,9 @@ export class AuthService {
localStorage.setItem('accessToken', data.accessToken);
}
return response.data;
} catch (error: any) {
const message = error.response?.data?.message || 'نام کاربری یا رمز عبور اشتباه است';
} catch (error) {
const err = error as ApiErr;
const message = err.response?.data?.message || 'نام کاربری یا رمز عبور اشتباه است';
throw new Error(Array.isArray(message) ? message[0] : message);
}
}
@ -106,11 +119,12 @@ export class AuthService {
try {
const response = await api.get('/users/profile');
return response.data;
} catch (error: any) {
if (error.response?.status === 401 || error.response?.status === 403) {
} catch (error) {
const err = error as ApiErr;
if (err.response?.status === 401 || err.response?.status === 403) {
return null;
}
const message = error.response?.data?.message || 'خطا در دریافت اطلاعات کاربری';
const message = err.response?.data?.message || 'خطا در دریافت اطلاعات کاربری';
throw new Error(Array.isArray(message) ? message[0] : message);
}
}
@ -122,8 +136,9 @@ export class AuthService {
try {
const response = await api.patch('/users/profile', profileData);
return response.data;
} catch (error: any) {
const message = error.response?.data?.message || 'خطا در ویرایش اطلاعات کاربری';
} catch (error) {
const err = error as ApiErr;
const message = err.response?.data?.message || 'خطا در ویرایش اطلاعات کاربری';
throw new Error(Array.isArray(message) ? message[0] : message);
}
}

View File

@ -1,12 +1,11 @@
import api from './api';
import { CartItem } from '../store/cartStore';
export interface OrderItem {
id: string;
orderId: string;
productId: string | null;
quantity: number;
product?: any;
product?: Record<string, unknown>;
}
export interface Order {
@ -21,6 +20,14 @@ export interface Order {
orderItems: OrderItem[];
}
interface ApiErr {
response?: {
data?: {
message?: string | string[];
};
};
}
export class OrderService {
private static instance: OrderService;
@ -50,8 +57,9 @@ export class OrderService {
try {
const response = await api.post('/orders', orderData);
return response.data;
} catch (error: any) {
const message = error.response?.data?.message || 'خطا در ثبت سفارش';
} catch (error) {
const err = error as ApiErr;
const message = err.response?.data?.message || 'خطا در ثبت سفارش';
throw new Error(Array.isArray(message) ? message[0] : message);
}
}
@ -63,8 +71,9 @@ export class OrderService {
try {
const response = await api.get('/orders');
return response.data.data;
} catch (error: any) {
const message = error.response?.data?.message || 'خطا در دریافت لیست سفارش‌ها';
} catch (error) {
const err = error as ApiErr;
const message = err.response?.data?.message || 'خطا در دریافت لیست سفارش‌ها';
throw new Error(Array.isArray(message) ? message[0] : message);
}
}
@ -76,8 +85,9 @@ export class OrderService {
try {
const response = await api.get(`/orders/${id}`);
return response.data;
} catch (error: any) {
const message = error.response?.data?.message || 'خطا در دریافت جزئیات سفارش';
} catch (error) {
const err = error as ApiErr;
const message = err.response?.data?.message || 'خطا در دریافت جزئیات سفارش';
throw new Error(Array.isArray(message) ? message[0] : message);
}
}

View File

@ -23,18 +23,19 @@ export class ProductService {
return ProductService.instance;
}
private mapBackendToFrontend(data: any): Product {
private mapBackendToFrontend(inputData: Record<string, unknown>): Product {
const data = inputData as any;
// Find the local static product to inherit functions like calculateDosage
const local = PRODUCTS.find(p =>
p.artNo === data.artNo ||
p.id === data.productGroup ||
(data.slug && data.slug.includes(p.id))
(typeof data.slug === 'string' && data.slug.includes(p.id))
);
const safeParse = (val: any, fallback: any) => {
const safeParse = (val: unknown, fallback: unknown) => {
if (!val) return fallback;
if (typeof val === 'string') {
try { return JSON.parse(val); } catch (e) { return fallback; }
try { return JSON.parse(val); } catch { return fallback; }
}
return val;
};
@ -68,8 +69,8 @@ export class ProductService {
? data.imageUrl
: (local?.image || (data.imageUrl ? `/api${data.imageUrl}` : data.image || '')),
main_ingredients: data.ingredientList?.map((i: any) => i.ingredient) || data.ingredients?.split(/[،,-]/).map((s: string) => s.trim()).filter(Boolean) || [],
symptoms: data.symptoms?.map((s: any) => s.symptom || s) || [],
main_ingredients: data.ingredientList?.map((i: { ingredient: string }) => i.ingredient) || data.ingredients?.split(/[،,-]/).map((s: string) => s.trim()).filter(Boolean) || [],
symptoms: data.symptoms?.map((s: { symptom?: string }) => s.symptom || s) || [],
keyBenefits: safeParse(data.keyBenefits, []),
expectedResults: safeParse(data.expectedResults, []),
benefitsList: safeParse(data.benefitsList, []),
@ -102,7 +103,7 @@ export class ProductService {
try {
const response = await api.get(`/products?${params.toString()}`);
return {
data: response.data.data.map((item: any) => this.mapBackendToFrontend(item)),
data: response.data.data.map((item: Record<string, unknown>) => this.mapBackendToFrontend(item)),
meta: response.data.meta,
};
} catch (error) {
@ -134,7 +135,7 @@ export class ProductService {
public async getFeaturedProducts(): Promise<Product[]> {
try {
const response = await api.get('/products?limit=30');
const all = response.data.data.map((item: any) => this.mapBackendToFrontend(item));
const all = response.data.data.map((item: Record<string, unknown>) => this.mapBackendToFrontend(item));
const featured: Product[] = [];
const categoriesSeen = new Set<string>();

View File

@ -32,11 +32,11 @@ const mockProduct = {
dosage_logic: '1',
benefits: '',
symptoms: [],
suitableFor: 'سگ' as any,
suitableFor: 'سگ' as never,
calculateDosage: () => ({ quantity: 1, unit: 'tablet', description: '' }),
analysis: {},
feedingAdvice: '',
specialist: null as any,
specialist: null as never,
image: '',
};

View File

@ -44,7 +44,7 @@ interface CartStore {
addOrder: (orderData: Omit<Order, 'id' | 'date' | 'status' | 'trackingNumber'>) => Promise<string>;
clearCart: () => void;
removeCoupon: () => void;
setOrders: (orders: any[]) => void;
setOrders: (orders: Record<string, unknown>[]) => void;
getTotalItems: () => number;
getSubtotal: () => number;
getDiscount: () => number;
@ -120,6 +120,7 @@ export const useCartStore = create<CartStore>()(
try {
const backendOrder = await orderService.createOrder(payload);
const bo = backendOrder as unknown as Record<string, unknown>;
const newOrder: Order = {
...orderData,
@ -127,15 +128,15 @@ export const useCartStore = create<CartStore>()(
date: backendOrder.createdAt,
total: Number(backendOrder.totalAmount),
charityDonation: Number(backendOrder.charityDonation),
status: backendOrder.status as any,
isRefill: (backendOrder as any).isRefill !== undefined ? Boolean((backendOrder as any).isRefill) : Boolean(orderData.isRefill || get().isSubscribed),
paymentMethod: (backendOrder as any).paymentMethod || orderData.paymentMethod,
status: (['processing', 'shipped', 'delivered'].includes(backendOrder.status) ? backendOrder.status : 'processing') as Order['status'],
isRefill: bo.isRefill !== undefined ? Boolean(bo.isRefill) : Boolean(orderData.isRefill || get().isSubscribed),
paymentMethod: (bo.paymentMethod as string) || orderData.paymentMethod,
trackingNumber: backendOrder.trackingNumber || `CN-${Math.floor(Math.random() * 90000) + 10000}`
};
set({ orders: [newOrder, ...get().orders] });
return backendOrder.id;
} catch (error: any) {
} catch (error) {
console.error("Order creation failed on backend:", error);
throw error;
}
@ -144,20 +145,20 @@ export const useCartStore = create<CartStore>()(
removeCoupon: () => set({ coupon: null }),
setOrders: (backendOrders) => {
const mappedOrders: Order[] = backendOrders.map(bo => ({
id: bo.id,
date: bo.createdAt,
items: bo.orderItems?.map((oi: any) => ({
id: String(bo.id),
date: String(bo.createdAt),
items: (bo.orderItems as Array<{ product: Product; quantity: number }>)?.map((oi) => ({
product: oi.product,
quantity: oi.quantity
})) || [],
total: Number(bo.totalAmount),
charityDonation: Number(bo.charityDonation),
status: bo.status,
isRefill: Boolean((bo as any).isRefill),
paymentMethod: (bo as any).paymentMethod,
petId: bo.petId || bo.pet?.id,
shippingAddress: bo.shippingAddress,
trackingNumber: bo.trackingNumber || `CN-${Math.floor(Math.random() * 90000) + 10000}`
status: (['processing', 'shipped', 'delivered'].includes(String(bo.status)) ? bo.status : 'processing') as Order['status'],
isRefill: Boolean(bo.isRefill),
paymentMethod: bo.paymentMethod as string | undefined,
petId: (bo.petId as string) || ((bo.pet as { id?: string })?.id),
shippingAddress: bo.shippingAddress as string | undefined,
trackingNumber: (bo.trackingNumber as string) || `CN-${Math.floor(Math.random() * 90000) + 10000}`
}));
set({ orders: mappedOrders });
},

View File

@ -4,9 +4,9 @@ interface UIState {
isCartOpen: boolean;
isLoginModalOpen: boolean;
isB2BPortalOpen: boolean;
advisorData: any;
advisorData: Record<string, unknown> | null;
setCartOpen: (open: boolean) => void;
setLoginModalOpen: (open: boolean, advisorData?: any) => void;
setLoginModalOpen: (open: boolean, advisorData?: Record<string, unknown> | null) => void;
setB2BPortalOpen: (open: boolean) => void;
clearAdvisorData: () => void;
}

View File

@ -3,6 +3,7 @@ import { create } from "zustand";
import { persist } from "zustand/middleware";
import api from "../services/api";
import { toast } from "sonner";
import { useUserStore } from "./userStore";
export interface Reminder {
id: string;
@ -54,7 +55,7 @@ interface PetStore {
addReminder: (petId: string, reminder: Omit<Reminder, "id" | "completedDates">) => Promise<void>;
toggleReminder: (petId: string, reminderId: string, date: string) => Promise<void>;
addHealthLog: (petId: string, log: Omit<HealthLog, "id" | "date">) => Promise<void>;
setPets: (pets: any[]) => void;
setPets: (pets: Record<string, unknown>[]) => void;
reset: () => void;
}
@ -125,7 +126,6 @@ export const usePetStore = create<PetStore>()(
getActivePet: () => {
const { pets, activePetId } = get();
try {
const { useUserStore } = require("./userStore");
if (!useUserStore.getState().isLoggedIn) {
return null;
}
@ -195,29 +195,29 @@ export const usePetStore = create<PetStore>()(
const mappedPets: PetProfile[] = backendPets.map(bp => {
const existingLocal = currentLocalPets.find(lp => lp.id === bp.id);
return {
id: bp.id,
name: bp.name,
type: bp.type as any,
breed: bp.breed,
age: Number(bp.age),
weight: Number(bp.weight),
activityLevel: bp.activityLevel as any,
medicalConditions: bp.medicalConditions?.map((mc: any) => mc.condition) || [],
image: bp.imageUrl || undefined,
reminders: bp.reminders?.map((r: any) => ({
id: String(bp.id),
name: String(bp.name),
type: (bp.type as PetProfile['type']) || "سگ",
breed: String(bp.breed || ''),
age: Number(bp.age || 0),
weight: Number(bp.weight || 0),
activityLevel: (bp.activityLevel as PetProfile['activityLevel']) || "متوسط",
medicalConditions: (bp.medicalConditions as Array<{ condition: string }>)?.map((mc) => mc.condition) || [],
image: (bp.imageUrl as string) || undefined,
reminders: (bp.reminders as Array<{ id: string; title: string; time: string; frequency: Reminder['frequency']; productId?: string; completions?: Array<{ completedDate: string }> }>)?.map((r) => ({
id: r.id,
title: r.title,
time: r.time,
frequency: r.frequency as any,
frequency: r.frequency,
productId: r.productId,
completedDates: r.completions?.map((c: any) => c.completedDate) || []
completedDates: r.completions?.map((c) => c.completedDate) || []
})) || [],
logs: bp.healthLogs?.map((hl: any) => ({
logs: (bp.healthLogs as Array<{ id: string; loggedDate: string; appetite: HealthLog['appetite']; energy: HealthLog['energy']; digestion: HealthLog['digestion']; note?: string }>)?.map((hl) => ({
id: hl.id,
date: hl.loggedDate,
appetite: hl.appetite as any,
energy: hl.energy as any,
digestion: hl.digestion as any,
appetite: hl.appetite,
energy: hl.energy,
digestion: hl.digestion,
note: hl.note || undefined
})) || [],
consumptions: existingLocal?.consumptions || []

View File

@ -94,7 +94,16 @@ export const useUserStore = create<UserStore>()(
addAddress: async (address) => {
try {
// Remove ID so backend generates UUID
const { id, ...addressData } = address;
const addressData = {
title: address.title,
receptorName: address.receptorName,
phone: address.phone,
province: address.province,
city: address.city,
detail: address.detail,
zipCode: address.zipCode,
isDefault: address.isDefault,
};
await api.post('/users/addresses', addressData);
await useUserStore.getState().fetchProfile();
} catch (error) {
@ -104,7 +113,16 @@ export const useUserStore = create<UserStore>()(
},
updateAddress: async (id, updated) => {
try {
const { id: _, ...addressData } = updated;
const addressData = {
title: updated.title,
receptorName: updated.receptorName,
phone: updated.phone,
province: updated.province,
city: updated.city,
detail: updated.detail,
zipCode: updated.zipCode,
isDefault: updated.isDefault,
};
await api.patch(`/users/addresses/${id}`, addressData);
await useUserStore.getState().fetchProfile();
} catch (error) {
@ -201,14 +219,15 @@ export const useUserStore = create<UserStore>()(
return;
}
set((state) => {
const pd = profileData as unknown as Record<string, unknown>;
const backendWallet = Number(profileData.walletBalance || 0);
const backendCharity = Number(profileData.charityDonationTotal || 0);
const backendTransactions = (profileData as any).walletTransactions?.map((t: any) => ({
const backendTransactions = (pd.walletTransactions as Array<{ id: string; type: string; amount: number; createdAt: string; status: string }>)?.map((t) => ({
id: t.id,
type: t.type === 'deposit' ? 'top_up' : 'purchase',
type: (t.type === 'deposit' ? 'top_up' : 'purchase') as Transaction['type'],
amount: Number(t.amount),
date: t.createdAt,
status: t.status === 'completed' ? 'success' : t.status === 'failed' ? 'failed' : 'pending'
status: (t.status === 'completed' ? 'success' : t.status === 'failed' ? 'failed' : 'pending') as Transaction['status']
})) || [];
return {
@ -221,7 +240,7 @@ export const useUserStore = create<UserStore>()(
mobile: profileData.mobile || "",
walletBalance: backendWallet,
charityDonationTotal: backendCharity,
addresses: (profileData as any).addresses?.length > 0 ? (profileData as any).addresses : state.profile.addresses,
addresses: Array.isArray(pd.addresses) && pd.addresses.length > 0 ? (pd.addresses as Address[]) : state.profile.addresses,
transactions: backendTransactions.length > 0 ? backendTransactions : state.profile.transactions
}
};
@ -230,11 +249,11 @@ export const useUserStore = create<UserStore>()(
// Sync orders list to CartStore while preserving local petId & address mappings
if (profileData.orders) {
const currentLocalOrders = useCartStore.getState().orders;
const mergedOrders = profileData.orders.map((bo: any) => {
const mergedOrders = (profileData.orders as Record<string, unknown>[]).map((bo) => {
const localMatch = currentLocalOrders.find(lo => lo.id === bo.id);
return {
...bo,
petId: bo.petId || bo.pet?.id || localMatch?.petId,
petId: bo.petId || (bo.pet as { id?: string })?.id || localMatch?.petId,
shippingAddress: bo.shippingAddress || localMatch?.shippingAddress
};
});
@ -242,7 +261,7 @@ export const useUserStore = create<UserStore>()(
}
// Sync pets list to PetStore
if (profileData.pets) {
usePetStore.getState().setPets(profileData.pets);
usePetStore.getState().setPets(profileData.pets as Record<string, unknown>[]);
}
} catch (error) {
console.error("Failed to fetch user profile:", error);

View File

@ -3,6 +3,12 @@ export const toPersian = (n: number | string | undefined | null) => {
return n.toString().replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d)]);
};
export const toEnglishDigits = (str: string) => {
if (!str) return "";
return str.replace(/[۰-۹]/g, d => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d).toString());
};
export function cn(...classes: (string | boolean | undefined)[]) {
return classes.filter(Boolean).join(" ");
}