Compare commits
4 Commits
b96379b88b
...
5e08c8d23e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e08c8d23e | ||
|
|
7bf6c47ee6 | ||
|
|
8b1c8d4053 | ||
|
|
f8e9ebfc44 |
@ -128,6 +128,7 @@ export default function Products() {
|
||||
shortDescription: product.shortDescription || '',
|
||||
categoryId: product.categoryId || '',
|
||||
priceValue: Number(product.priceValue),
|
||||
wholesalePrice: product.wholesalePrice ? Number(product.wholesalePrice) : '',
|
||||
priceDisplay: product.priceDisplay || '',
|
||||
unit: product.unit || '',
|
||||
packageSize: Number(product.packageSize),
|
||||
@ -145,7 +146,7 @@ export default function Products() {
|
||||
setEditingProduct(null);
|
||||
setFormData({
|
||||
artNo: '', nameFa: '', nameEn: '', scientificTagline: '', description: '', shortDescription: '', categoryId: '',
|
||||
priceValue: 0, priceDisplay: '', unit: '', packageSize: 0, dosageLogic: '', suitableFor: 'سگ و گربه',
|
||||
priceValue: 0, wholesalePrice: '', priceDisplay: '', unit: '', packageSize: 0, dosageLogic: '', suitableFor: 'سگ و گربه',
|
||||
imageUrl: '', metaTitle: '', metaDescription: '', keywords: '', canonicalUrl: '', slug: '',
|
||||
symptoms: []
|
||||
});
|
||||
@ -535,6 +536,13 @@ export default function Products() {
|
||||
<label className="text-sm font-bold text-gray-700">قیمت (تومان) *</label>
|
||||
<input required type="number" min="0" value={formData.priceValue} onChange={(e) => setFormData({...formData, priceValue: 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-purple-700 flex items-center gap-1">
|
||||
<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" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-bold text-gray-700">نمایش قیمت (متنی) *</label>
|
||||
<input required type="text" placeholder="مثال: ۸۵۰,۰۰۰ تومان" value={formData.priceDisplay} onChange={(e) => setFormData({...formData, priceDisplay: e.target.value})} className="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none" />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Settings as SettingsIcon, Save, Truck, ShieldAlert, Percent, AlertCircle } from 'lucide-react';
|
||||
import { Settings as SettingsIcon, Save, Truck, ShieldAlert, Percent, AlertCircle, CreditCard, Share2 } from 'lucide-react';
|
||||
import api from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
|
||||
@ -10,6 +10,16 @@ export default function Settings() {
|
||||
B2B_DISCOUNT_PERCENT: '',
|
||||
MAINTENANCE_MODE: 'false',
|
||||
CHARITY_ROUND_STEP: '10000',
|
||||
PAY_GATEWAY_CARD_ENABLE: 'true',
|
||||
PAY_GATEWAY_WALLET_ENABLE: 'true',
|
||||
PAY_GATEWAY_ONLINE_ENABLE: 'false',
|
||||
PAY_GATEWAY_COD_ENABLE: 'false',
|
||||
CONTACT_PHONE: '۰۲۱-۸۸۸۸ ۴۴۴۴',
|
||||
CONTACT_EMAIL: 'info@canina-iran.com',
|
||||
SOCIAL_WHATSAPP: '09120000000',
|
||||
SOCIAL_INSTAGRAM: 'https://instagram.com/canina_iran',
|
||||
SOCIAL_TELEGRAM: 'https://t.me/canina_iran',
|
||||
CONTACT_ADDRESS: 'تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹',
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@ -25,6 +35,16 @@ export default function Settings() {
|
||||
B2B_DISCOUNT_PERCENT: response.data.data.B2B_DISCOUNT_PERCENT || '0',
|
||||
MAINTENANCE_MODE: response.data.data.MAINTENANCE_MODE || 'false',
|
||||
CHARITY_ROUND_STEP: response.data.data.CHARITY_ROUND_STEP || '10000',
|
||||
PAY_GATEWAY_CARD_ENABLE: response.data.data.PAY_GATEWAY_CARD_ENABLE || 'true',
|
||||
PAY_GATEWAY_WALLET_ENABLE: response.data.data.PAY_GATEWAY_WALLET_ENABLE || 'true',
|
||||
PAY_GATEWAY_ONLINE_ENABLE: response.data.data.PAY_GATEWAY_ONLINE_ENABLE || 'false',
|
||||
PAY_GATEWAY_COD_ENABLE: response.data.data.PAY_GATEWAY_COD_ENABLE || 'false',
|
||||
CONTACT_PHONE: response.data.data.CONTACT_PHONE || '۰۲۱-۸۸۸۸ ۴۴۴۴',
|
||||
CONTACT_EMAIL: response.data.data.CONTACT_EMAIL || 'info@canina-iran.com',
|
||||
SOCIAL_WHATSAPP: response.data.data.SOCIAL_WHATSAPP || '09120000000',
|
||||
SOCIAL_INSTAGRAM: response.data.data.SOCIAL_INSTAGRAM || 'https://instagram.com/canina_iran',
|
||||
SOCIAL_TELEGRAM: response.data.data.SOCIAL_TELEGRAM || 'https://t.me/canina_iran',
|
||||
CONTACT_ADDRESS: response.data.data.CONTACT_ADDRESS || 'تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@ -139,6 +159,156 @@ export default function Settings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Gateways Settings */}
|
||||
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-6 lg:col-span-2">
|
||||
<h3 className="text-lg font-bold text-gray-900 border-b border-gray-100 pb-4 flex items-center gap-2">
|
||||
<CreditCard className="w-5 h-5 text-indigo-500" />
|
||||
مدیریت روشهای پرداخت
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-xl border border-gray-200">
|
||||
<div>
|
||||
<h4 className="font-bold text-gray-900 text-sm">کارت به کارت (واریز مستقیم)</h4>
|
||||
<p className="text-xs text-gray-500 mt-0.5">واریز به شماره کارت/شبا رسمی کانینا</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={settings.PAY_GATEWAY_CARD_ENABLE === 'true'}
|
||||
onChange={(e) => setSettings({ ...settings, PAY_GATEWAY_CARD_ENABLE: e.target.checked ? 'true' : 'false' })}
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-xl border border-gray-200">
|
||||
<div>
|
||||
<h4 className="font-bold text-gray-900 text-sm">اعتبار حساب (کیف پول)</h4>
|
||||
<p className="text-xs text-gray-500 mt-0.5">پرداخت لحظهای از موجودی کیف پول کاربر</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={settings.PAY_GATEWAY_WALLET_ENABLE === 'true'}
|
||||
onChange={(e) => setSettings({ ...settings, PAY_GATEWAY_WALLET_ENABLE: e.target.checked ? 'true' : 'false' })}
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-xl border border-gray-200">
|
||||
<div>
|
||||
<h4 className="font-bold text-gray-900 text-sm">درگاه آنلاین شتاب</h4>
|
||||
<p className="text-xs text-gray-500 mt-0.5">پرداخت الکترونیک (در صورت غیرفعال بودن: برچسب به زودی)</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={settings.PAY_GATEWAY_ONLINE_ENABLE === 'true'}
|
||||
onChange={(e) => setSettings({ ...settings, PAY_GATEWAY_ONLINE_ENABLE: e.target.checked ? 'true' : 'false' })}
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between p-4 bg-gray-50 rounded-xl border border-gray-200">
|
||||
<div>
|
||||
<h4 className="font-bold text-gray-900 text-sm">پرداخت در محل (COD)</h4>
|
||||
<p className="text-xs text-gray-500 mt-0.5">پرداخت با کارتخوان هنگام تحویل (در صورت غیرفعال بودن: به زودی)</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={settings.PAY_GATEWAY_COD_ENABLE === 'true'}
|
||||
onChange={(e) => setSettings({ ...settings, PAY_GATEWAY_COD_ENABLE: e.target.checked ? 'true' : 'false' })}
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social Media & Contact Info Settings */}
|
||||
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-6 lg:col-span-2">
|
||||
<h3 className="text-lg font-bold text-gray-900 border-b border-gray-100 pb-4 flex items-center gap-2">
|
||||
<Share2 className="w-5 h-5 text-blue-600" />
|
||||
راههای ارتباطی و شبکههای اجتماعی
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-700 mb-1">تلفن پشتیبانی و فروش</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.CONTACT_PHONE}
|
||||
onChange={(e) => setSettings({ ...settings, CONTACT_PHONE: e.target.value })}
|
||||
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold"
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-700 mb-1">ایمیل رسمی پشتیبانی</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.CONTACT_EMAIL}
|
||||
onChange={(e) => setSettings({ ...settings, CONTACT_EMAIL: e.target.value })}
|
||||
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold"
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-700 mb-1">شماره واتساپ پشتیبانی</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.SOCIAL_WHATSAPP}
|
||||
onChange={(e) => setSettings({ ...settings, SOCIAL_WHATSAPP: e.target.value })}
|
||||
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold"
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-700 mb-1">لینک پیج اینستاگرام</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.SOCIAL_INSTAGRAM}
|
||||
onChange={(e) => setSettings({ ...settings, SOCIAL_INSTAGRAM: e.target.value })}
|
||||
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold"
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-700 mb-1">کانال تلگرام</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.SOCIAL_TELEGRAM}
|
||||
onChange={(e) => setSettings({ ...settings, SOCIAL_TELEGRAM: e.target.value })}
|
||||
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold"
|
||||
dir="ltr"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-gray-700 mb-1">آدرس دفتر مرکزی</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.CONTACT_ADDRESS}
|
||||
onChange={(e) => setSettings({ ...settings, CONTACT_ADDRESS: e.target.value })}
|
||||
className="w-full border border-gray-200 rounded-xl p-2.5 outline-none focus:ring-2 focus:ring-purple-500 text-xs font-bold"
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Settings */}
|
||||
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 space-y-6 lg:col-span-2">
|
||||
<h3 className="text-lg font-bold text-gray-900 border-b border-gray-100 pb-4 flex items-center gap-2">
|
||||
|
||||
@ -137,6 +137,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Sonner Toasts Vazirmatn Font */
|
||||
[data-sonner-toaster] {
|
||||
font-family: "Vazirmatn", sans-serif !important;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for a professional look */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Metadata } from 'next';
|
||||
import EnamadBadge from '@/components/EnamadBadge';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import { ShieldCheck, Award, FileCheck, CheckCircle2, Building2, ExternalLink } from 'lucide-react';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@ -47,9 +45,7 @@ export default function TrustSealsPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 flex flex-col font-vazir">
|
||||
<Header />
|
||||
|
||||
<div className="bg-slate-50 flex flex-col font-vazir">
|
||||
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
{/* Banner Section */}
|
||||
<div className="relative bg-gradient-to-r from-teal-700 via-teal-600 to-emerald-700 rounded-3xl p-8 md:p-12 text-white shadow-xl overflow-hidden mb-12">
|
||||
@ -129,8 +125,6 @@ export default function TrustSealsPage() {
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { X, MapPin, User, Phone, Hash, Save, Check } from "lucide-react";
|
||||
import { toPersian, cn } from "../lib/utils";
|
||||
import { Address } from "../lib/store/userStore";
|
||||
import { IRAN_PROVINCES, PROVINCE_CITIES } from "../lib/data/provinces";
|
||||
import SearchableSelect from "./SearchableSelect";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface AddressModalProps {
|
||||
@ -206,12 +207,13 @@ export default function AddressModal({ isOpen, onClose, onSave, editingAddress }
|
||||
{/* Row 3: Province & City */}
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="addr-province" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">استان</label>
|
||||
<select
|
||||
<SearchableSelect
|
||||
id="addr-province"
|
||||
name="address-level1"
|
||||
options={IRAN_PROVINCES}
|
||||
value={formData.province}
|
||||
onChange={e => {
|
||||
const newProv = e.target.value;
|
||||
placeholder="انتخاب استان..."
|
||||
searchPlaceholder="جستجوی استان..."
|
||||
onChange={(newProv) => {
|
||||
const cities = PROVINCE_CITIES[newProv] || [];
|
||||
setFormData({
|
||||
...formData,
|
||||
@ -219,31 +221,21 @@ export default function AddressModal({ isOpen, onClose, onSave, editingAddress }
|
||||
city: cities[0] || ""
|
||||
});
|
||||
}}
|
||||
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20 bg-white", errors.province ? "border-red-300" : "border-medical-gray-100")}
|
||||
>
|
||||
<option value="">انتخاب استان...</option>
|
||||
{IRAN_PROVINCES.map(prov => (
|
||||
<option key={prov} value={prov}>{prov}</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
{errors.province && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.province}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="addr-city" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">شهر</label>
|
||||
{formData.province && PROVINCE_CITIES[formData.province] ? (
|
||||
<select
|
||||
<SearchableSelect
|
||||
id="addr-city"
|
||||
name="address-level2"
|
||||
options={PROVINCE_CITIES[formData.province]}
|
||||
value={formData.city}
|
||||
onChange={e => setFormData({...formData, city: e.target.value})}
|
||||
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20 bg-white", errors.city ? "border-red-300" : "border-medical-gray-100")}
|
||||
>
|
||||
<option value="">انتخاب شهر...</option>
|
||||
{PROVINCE_CITIES[formData.province].map(c => (
|
||||
<option key={c} value={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
placeholder="انتخاب شهر..."
|
||||
searchPlaceholder="جستجوی شهر..."
|
||||
onChange={(c) => setFormData({...formData, city: c})}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
id="addr-city"
|
||||
|
||||
@ -3,6 +3,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { Product } from "../lib/data/products";
|
||||
import { useCartStore } from "../lib/store/cartStore";
|
||||
import { useUserStore } from "../lib/store/userStore";
|
||||
import { useSettingsStore } from "../lib/store/settingsStore";
|
||||
import { productService } from "../lib/services/productService";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import {
|
||||
@ -127,27 +128,29 @@ export default function B2BPortal({ onClose }: { onClose: () => void }) {
|
||||
</p>
|
||||
|
||||
{!isWholesaleVerified && (
|
||||
<div className="bg-white/10 p-4 rounded-2xl border border-white/20 mt-4">
|
||||
<h4 className="text-xs font-black mb-2 text-amber-300">درخواست تایید حساب همکار</h4>
|
||||
<p className="text-[11px] text-white/80 mb-3">جهت مشاهده و خرید با قیمت عمده، شماره پروانه نظام دامپزشکی خود را وارد کنید.</p>
|
||||
<div className="bg-white/10 p-4 rounded-2xl border border-white/20 mt-4 font-vazir">
|
||||
<h4 className="text-xs font-black mb-2 text-amber-300">درخواست مشاوره و عضویت همکار B2B</h4>
|
||||
<p className="text-[11px] text-white/80 mb-3 leading-relaxed">
|
||||
جهت دریافت شرایط همکاری و فعالسازی دسترسی خرید عمده، شماره تماس خود را وارد کنید تا کارشناسان فروش با شما تماس بگیرند.
|
||||
</p>
|
||||
<form onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target as HTMLFormElement;
|
||||
const bName = (form.elements.namedItem('bName') as HTMLInputElement).value;
|
||||
const lNum = (form.elements.namedItem('lNum') as HTMLInputElement).value;
|
||||
const phone = (form.elements.namedItem('phone') as HTMLInputElement).value;
|
||||
try {
|
||||
const api = (await import('../lib/services/api')).default;
|
||||
await api.post('/wholesale/apply', { businessName: bName, licenseNumber: lNum });
|
||||
alert('درخواست ثبتنام همکار با موفقیت ارسال شد و به زودی توسط ادمین تایید میشود.');
|
||||
await api.post('/wholesale/apply', { businessName: bName, licenseNumber: phone });
|
||||
alert('درخواست شما با موفقیت ثبت شد. کارشناسان پشتیبانی B2B کانینا به زودی جهت تایید تلفنی با شما تماس خواهند گرفت.');
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
alert('لطفاً ابتدا وارد حساب کاربری خود شوید.');
|
||||
alert('خطا در ثبت درخواست. لطفاً دوباره تلاش کنید.');
|
||||
}
|
||||
}} className="space-y-2">
|
||||
<input name="bName" required placeholder="نام کلینیک / داروخانه" className="w-full bg-white/20 text-white text-xs px-3 py-2 rounded-xl outline-none placeholder:text-white/50" />
|
||||
<input name="lNum" required placeholder="شماره پروانه / نظام" className="w-full bg-white/20 text-white text-xs px-3 py-2 rounded-xl outline-none placeholder:text-white/50" />
|
||||
<button type="submit" className="w-full bg-amber-400 hover:bg-amber-500 text-gray-900 font-black text-xs py-2 rounded-xl transition-colors">
|
||||
ثبت درخواست همکار
|
||||
<input name="bName" required placeholder="نام مجموعه / کلینیک / داروخانه" className="w-full bg-white/20 text-white text-xs px-3 py-2.5 rounded-xl outline-none placeholder:text-white/50" />
|
||||
<input name="phone" required type="tel" placeholder="شماره تماس همراه" className="w-full bg-white/20 text-white text-xs px-3 py-2.5 rounded-xl outline-none placeholder:text-white/50 dir-ltr text-right" />
|
||||
<button type="submit" className="w-full bg-amber-400 hover:bg-amber-500 text-gray-900 font-black text-xs py-2.5 rounded-xl transition-colors">
|
||||
ثبت درخواست تماس و تایید B2B
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -209,7 +212,11 @@ export default function B2BPortal({ onClose }: { onClose: () => void }) {
|
||||
<tbody className="divide-y divide-medical-gray-100">
|
||||
{filteredProducts.map(product => {
|
||||
const basePrice = product.priceValue || 0;
|
||||
const wholesalePrice = basePrice * 0.7; // 30% discount
|
||||
// Priority 1: Specific product wholesalePrice, Priority 2: General settings percentage discount (default 30%)
|
||||
const wholesaleDiscountPercent = Number(useSettingsStore.getState().getText('B2B_DISCOUNT_PERCENT', '30')) || 30;
|
||||
const wholesalePrice = (product as any).wholesalePrice
|
||||
? Number((product as any).wholesalePrice)
|
||||
: Math.round(basePrice * (1 - wholesaleDiscountPercent / 100));
|
||||
return (
|
||||
<tr key={product.id} className="hover:bg-medical-gray-50/50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
|
||||
@ -51,6 +51,18 @@ export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate
|
||||
}
|
||||
}, [hasLachsOl, hasCanhydrox]);
|
||||
|
||||
// Lock background body scroll when cart drawer is open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "unset";
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = "unset";
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
|
||||
@ -26,6 +26,7 @@ import { useSettingsStore } from "../lib/store/settingsStore";
|
||||
import { productService } from "../lib/services/productService";
|
||||
import TopUpModal from "./TopUpModal";
|
||||
import AddressModal from "./AddressModal";
|
||||
import SearchableSelect from "./SearchableSelect";
|
||||
import { IRAN_PROVINCES, PROVINCE_CITIES } from "../lib/data/provinces";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from 'next/navigation';
|
||||
@ -50,6 +51,7 @@ export default function CheckoutPage() {
|
||||
const [manualPhone, setManualPhone] = useState('');
|
||||
const [manualProvince, setManualProvince] = useState('');
|
||||
const [manualCity, setManualCity] = useState('');
|
||||
const [manualZipCode, setManualZipCode] = useState('');
|
||||
const [manualAddress, setManualAddress] = useState('');
|
||||
|
||||
const handleCopy = (text: string, title: string) => {
|
||||
@ -120,10 +122,10 @@ export default function CheckoutPage() {
|
||||
const selectedAddr = profile.addresses?.find(a => a.id === selectedAddressId);
|
||||
let shippingAddressStr = '';
|
||||
if (selectedAddr) {
|
||||
shippingAddressStr = `${selectedAddr.province}، ${selectedAddr.city}، ${selectedAddr.detail} (گیرنده: ${selectedAddr.receptorName} - ${toPersian(selectedAddr.phone)})`;
|
||||
shippingAddressStr = `${selectedAddr.province}، ${selectedAddr.city}، ${selectedAddr.detail}${selectedAddr.zipCode ? ` (کد پستی: ${selectedAddr.zipCode})` : ''} (گیرنده: ${selectedAddr.receptorName} - ${toPersian(selectedAddr.phone)})`;
|
||||
} else if (manualAddress) {
|
||||
const provCity = [manualProvince, manualCity].filter(Boolean).join('، ');
|
||||
shippingAddressStr = `${provCity ? provCity + '، ' : ''}${manualAddress}${manualName ? ` (گیرنده: ${manualName}${manualPhone ? ` - ${toPersian(manualPhone)}` : ''})` : ''}`;
|
||||
shippingAddressStr = `${provCity ? provCity + '، ' : ''}${manualAddress}${manualZipCode ? ` (کد پستی: ${manualZipCode})` : ''}${manualName ? ` (گیرنده: ${manualName}${manualPhone ? ` - ${toPersian(manualPhone)}` : ''})` : ''}`;
|
||||
}
|
||||
|
||||
const orderId = await addOrder({
|
||||
@ -314,37 +316,30 @@ export default function CheckoutPage() {
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="chk-province" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">استان *</label>
|
||||
<select
|
||||
<SearchableSelect
|
||||
id="chk-province"
|
||||
options={IRAN_PROVINCES}
|
||||
value={manualProvince}
|
||||
onChange={(e) => {
|
||||
const prov = e.target.value;
|
||||
placeholder="انتخاب استان..."
|
||||
searchPlaceholder="جستجوی استان..."
|
||||
onChange={(prov) => {
|
||||
const cities = PROVINCE_CITIES[prov] || [];
|
||||
setManualProvince(prov);
|
||||
setManualCity(cities[0] || '');
|
||||
}}
|
||||
className="w-full bg-white border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20 font-bold"
|
||||
>
|
||||
<option value="">انتخاب استان...</option>
|
||||
{IRAN_PROVINCES.map((prov) => (
|
||||
<option key={prov} value={prov}>{prov}</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="chk-city" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">شهر *</label>
|
||||
{manualProvince && PROVINCE_CITIES[manualProvince] ? (
|
||||
<select
|
||||
<SearchableSelect
|
||||
id="chk-city"
|
||||
options={PROVINCE_CITIES[manualProvince]}
|
||||
value={manualCity}
|
||||
onChange={(e) => setManualCity(e.target.value)}
|
||||
className="w-full bg-white border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20 font-bold"
|
||||
>
|
||||
<option value="">انتخاب شهر...</option>
|
||||
{PROVINCE_CITIES[manualProvince].map((c) => (
|
||||
<option key={c} value={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
placeholder="انتخاب شهر..."
|
||||
searchPlaceholder="جستجوی شهر..."
|
||||
onChange={(c) => setManualCity(c)}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
id="chk-city"
|
||||
@ -368,6 +363,18 @@ export default function CheckoutPage() {
|
||||
placeholder="خیابان، کوچه، پلاک، واحد..."
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label htmlFor="chk-zipcode" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">کد پستی ۱۰ رقمی</label>
|
||||
<input
|
||||
id="chk-zipcode"
|
||||
type="text"
|
||||
maxLength={10}
|
||||
value={manualZipCode}
|
||||
onChange={(e) => setManualZipCode(e.target.value.replace(/[^0-9]/g, ''))}
|
||||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20 dir-ltr text-right"
|
||||
placeholder="۱۲۳۴۵۶۷۸۹۰"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -382,40 +389,80 @@ export default function CheckoutPage() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4">
|
||||
{[
|
||||
{(() => {
|
||||
const getText = useSettingsStore.getState().getText;
|
||||
const allGateways = [
|
||||
{
|
||||
id: 'card',
|
||||
label: 'کارت به کارت (واریز مستقیم)',
|
||||
desc: 'واریز به شماره کارت بانک سامان',
|
||||
icon: <CreditCard className="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
label: getText('PAY_GATEWAY_CARD_LABEL', 'کارت به کارت (واریز مستقیم)'),
|
||||
desc: getText('PAY_GATEWAY_CARD_DESC', 'واریز به شماره کارت بانک سامان'),
|
||||
icon: <CreditCard className="w-5 h-5 sm:w-6 sm:h-6" />,
|
||||
enabled: getText('PAY_GATEWAY_CARD_ENABLE', 'true') === 'true'
|
||||
},
|
||||
{
|
||||
id: 'wallet',
|
||||
label: 'اعتبار حساب (کیف پول)',
|
||||
label: getText('PAY_GATEWAY_WALLET_LABEL', 'اعتبار حساب (کیف پول)'),
|
||||
desc: `موجودی: ${toPersian((profile.walletBalance || 0).toLocaleString())} تومان`,
|
||||
icon: <Wallet className="w-5 h-5 sm:w-6 sm:h-6" />
|
||||
icon: <Wallet className="w-5 h-5 sm:w-6 sm:h-6" />,
|
||||
enabled: getText('PAY_GATEWAY_WALLET_ENABLE', 'true') === 'true'
|
||||
},
|
||||
{
|
||||
id: 'online',
|
||||
label: getText('PAY_GATEWAY_ONLINE_LABEL', 'درگاه پرداخت آنلاین (شتاب)'),
|
||||
desc: getText('PAY_GATEWAY_ONLINE_DESC', 'پرداخت مستقیم با کلیه کارتهای شتاب'),
|
||||
icon: <ShieldCheck className="w-5 h-5 sm:w-6 sm:h-6" />,
|
||||
enabled: getText('PAY_GATEWAY_ONLINE_ENABLE', 'false') === 'true'
|
||||
},
|
||||
{
|
||||
id: 'cod',
|
||||
label: getText('PAY_GATEWAY_COD_LABEL', 'پرداخت در محل (کارتخوان)'),
|
||||
desc: getText('PAY_GATEWAY_COD_DESC', 'تحویل کالا و پرداخت درب منزل'),
|
||||
icon: <Truck className="w-5 h-5 sm:w-6 sm:h-6" />,
|
||||
enabled: getText('PAY_GATEWAY_COD_ENABLE', 'false') === 'true'
|
||||
}
|
||||
].map((pay) => (
|
||||
];
|
||||
|
||||
const sortedGateways = [...allGateways].sort((a, b) => (b.enabled ? 1 : 0) - (a.enabled ? 1 : 0));
|
||||
|
||||
return sortedGateways.map((pay) => (
|
||||
<div
|
||||
key={pay.id}
|
||||
onClick={() => setPaymentMethod(pay.id)}
|
||||
className={cn("p-4 sm:p-6 rounded-2xl sm:rounded-3xl border-2 cursor-pointer transition-all group flex sm:flex-col items-center sm:items-start gap-4 sm:gap-0 relative", paymentMethod === pay.id ? "border-canina-blue bg-canina-blue/5" : "border-medical-gray-100 hover:border-canina-blue")}
|
||||
onClick={() => {
|
||||
if (pay.enabled) {
|
||||
setPaymentMethod(pay.id);
|
||||
} else {
|
||||
toast.info("این روش پرداخت به زودی فعال خواهد شد.");
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"p-4 sm:p-6 rounded-2xl sm:rounded-3xl border-2 transition-all group flex sm:flex-col items-center sm:items-start gap-4 sm:gap-0 relative",
|
||||
pay.enabled
|
||||
? (paymentMethod === pay.id ? "border-canina-blue bg-canina-blue/5 cursor-pointer" : "border-medical-gray-100 hover:border-canina-blue cursor-pointer")
|
||||
: "border-medical-gray-100 bg-medical-gray-50/60 opacity-60 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-medical-gray-50 rounded-xl sm:rounded-2xl flex items-center justify-center text-medical-gray-400 group-hover:text-canina-blue group-hover:bg-canina-blue/10 sm:mb-4 transition-all flex-shrink-0">
|
||||
{pay.icon}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="font-black text-xs sm:text-sm text-medical-gray-900 mb-0.5 sm:mb-1 truncate">{pay.label}</h4>
|
||||
<div className="min-w-0 flex-1 w-full">
|
||||
<div className="flex items-center justify-between gap-2 mb-0.5 sm:mb-1">
|
||||
<h4 className="font-black text-xs sm:text-sm text-medical-gray-900 truncate">{pay.label}</h4>
|
||||
{!pay.enabled && (
|
||||
<span className="text-[9px] font-black bg-amber-100 text-amber-800 px-2 py-0.5 rounded-full shrink-0 font-vazir">
|
||||
به زودی
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className={cn("text-[10px] font-bold truncate", pay.id === 'wallet' ? "text-canina-blue" : "text-medical-gray-400")}>{pay.desc}</p>
|
||||
{pay.id === 'wallet' && (profile.walletBalance || 0) === 0 && (
|
||||
{pay.id === 'wallet' && pay.enabled && (profile.walletBalance || 0) === 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowTopUpModal(true);
|
||||
}}
|
||||
className="text-[10px] bg-canina-blue text-white px-2 py-0.5 rounded-lg font-bold hover:bg-blue-700 transition-colors flex items-center gap-1"
|
||||
className="text-[10px] bg-canina-blue text-white px-2 py-0.5 rounded-lg font-bold hover:bg-blue-700 transition-colors flex items-center gap-1 shrink-0 font-vazir"
|
||||
>
|
||||
<PlusCircle className="w-3 h-3" />
|
||||
<span>افزایش موجودی</span>
|
||||
@ -424,7 +471,8 @@ export default function CheckoutPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{paymentMethod === 'card' && (
|
||||
|
||||
@ -134,7 +134,7 @@ export default function Footer({
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[11px] text-slate-400 font-extrabold uppercase mb-1">{getText('footer_phone_label', "خط ویژه فروش")}</p>
|
||||
<p className="text-lg font-extrabold tracking-widest text-left text-slate-200" dir="ltr">{getText('footer_phone', "۰۲۱-۸۸۸۸ ۴۴۴۴")}</p>
|
||||
<p className="text-lg font-extrabold tracking-widest text-left text-slate-200" dir="ltr">{getText('CONTACT_PHONE', getText('footer_phone', "۰۲۱-۸۸۸۸ ۴۴۴۴"))}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4 group">
|
||||
@ -143,7 +143,7 @@ export default function Footer({
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[11px] text-slate-400 font-extrabold uppercase mb-1">{getText('footer_email_label', "مکاتبات رسمی")}</p>
|
||||
<p className="text-sm font-bold text-slate-300">{getText('footer_email', "info@canina-iran.com")}</p>
|
||||
<p className="text-sm font-bold text-slate-300">{getText('CONTACT_EMAIL', getText('footer_email', "info@canina-iran.com"))}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -154,7 +154,7 @@ export default function Footer({
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[11px] text-slate-400 font-extrabold uppercase mb-1">{getText('footer_address_label', "دفتر مرکزی")}</p>
|
||||
<p className="text-sm font-bold leading-relaxed text-slate-300">{getText('footer_address', "تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹")}</p>
|
||||
<p className="text-sm font-bold leading-relaxed text-slate-300">{getText('CONTACT_ADDRESS', getText('footer_address', "تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹"))}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -152,16 +152,16 @@ export default function Header({
|
||||
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||
</button>
|
||||
|
||||
<Link href="/" className="flex items-center gap-3 group">
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-canina-blue rounded-2xl flex items-center justify-center text-white font-bold text-xl sm:text-2xl group-hover:bg-medical-gray-900 transition-all shadow-md italic">C</div>
|
||||
<div className="flex flex-col justify-center">
|
||||
<span className="text-canina-blue font-black text-lg sm:text-2xl tracking-tighter italic font-sans flex items-center gap-1 leading-none">
|
||||
<Link href="/" className="flex items-center gap-2 sm:gap-3 group shrink-0 min-w-0">
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 min-w-[40px] min-h-[40px] sm:min-w-[48px] sm:min-h-[48px] bg-canina-blue rounded-2xl flex items-center justify-center text-white font-bold text-xl sm:text-2xl group-hover:bg-medical-gray-900 transition-all shadow-md italic shrink-0">C</div>
|
||||
<div className="flex flex-col justify-center min-w-0">
|
||||
<span className="text-canina-blue font-black text-base sm:text-2xl tracking-tighter italic font-sans flex items-center gap-1 leading-none truncate">
|
||||
Canina
|
||||
<span className="text-xs sm:text-sm not-italic font-medium border-r border-medical-gray-300 pr-1.5 font-vazir text-medical-gray-700">
|
||||
<span className="text-[11px] sm:text-sm not-italic font-medium border-r border-medical-gray-300 pr-1.5 font-vazir text-medical-gray-700">
|
||||
{getText('brand_name_fa', "ایران")}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-[9px] text-medical-gray-400 font-bold uppercase tracking-wider leading-tight mt-0.5">
|
||||
<span className="hidden sm:block text-[9px] text-medical-gray-400 font-bold uppercase tracking-wider leading-tight mt-0.5 truncate">
|
||||
نماینده رسمی CANINA PHARMA GMBH GERMANY
|
||||
</span>
|
||||
</div>
|
||||
@ -185,7 +185,7 @@ export default function Header({
|
||||
</div>
|
||||
|
||||
{/* Left Action Buttons */}
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="flex items-center gap-1.5 sm:gap-3 shrink-0">
|
||||
{/* Prescription Upload Button */}
|
||||
<button
|
||||
onClick={() => setIsPrescriptionModalOpen(true)}
|
||||
@ -199,7 +199,8 @@ export default function Header({
|
||||
{isLoggedIn ? (
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="flex items-center gap-2 px-3 py-2 bg-medical-gray-50 hover:bg-medical-gray-100 border border-medical-gray-200 rounded-xl text-xs font-black text-medical-gray-900 transition-all"
|
||||
className="flex items-center justify-center p-2.5 sm:px-3 sm:py-2 bg-medical-gray-50 hover:bg-medical-gray-100 border border-medical-gray-200 rounded-xl text-xs font-black text-medical-gray-900 transition-all"
|
||||
title="حساب کاربری"
|
||||
>
|
||||
<User className="w-4 h-4 text-canina-blue" />
|
||||
<span className="hidden sm:inline">{profile.firstName || 'حساب کاربری'}</span>
|
||||
@ -207,10 +208,11 @@ export default function Header({
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setIsAuthModalOpen(true)}
|
||||
className="flex items-center gap-2 px-3.5 py-2 bg-medical-gray-900 text-white hover:bg-canina-blue rounded-xl text-xs font-black transition-all shadow-xs"
|
||||
className="flex items-center gap-1.5 px-2.5 py-2 sm:px-3.5 sm:py-2 bg-medical-gray-900 text-white hover:bg-canina-blue rounded-xl text-[11px] sm:text-xs font-black transition-all shadow-xs whitespace-nowrap"
|
||||
>
|
||||
<LogIn className="w-4 h-4" />
|
||||
<span>ورود / ثبتنام</span>
|
||||
<LogIn className="w-4 h-4 shrink-0" />
|
||||
<span className="hidden xs:inline sm:inline">ورود / ثبتنام</span>
|
||||
<span className="xs:hidden sm:hidden">ورود</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@ -370,11 +372,15 @@ export default function Header({
|
||||
<ChevronDown className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
{activeDropdown === 'company' && (
|
||||
<div className="absolute top-full right-0 w-56 bg-white border border-medical-gray-200 shadow-xl rounded-xl p-2 z-50 space-y-1 text-xs">
|
||||
<div className="absolute top-full right-0 w-60 bg-white border border-medical-gray-200 shadow-xl rounded-xl p-2 z-50 space-y-1 text-xs">
|
||||
<Link href="/about" className="flex items-center gap-2 px-3 py-2 hover:bg-canina-blue/5 hover:text-canina-blue rounded-lg font-bold">
|
||||
<Award className="w-4 h-4 text-canina-blue" />
|
||||
درباره کمپانی کانینا آلمان
|
||||
</Link>
|
||||
<Link href="/trust-seals" className="flex items-center gap-2 px-3 py-2 hover:bg-canina-blue/5 hover:text-canina-blue rounded-lg font-bold text-emerald-600">
|
||||
<ShieldCheck className="w-4 h-4 text-emerald-600" />
|
||||
نمادهای اعتماد و مجوزهای رسمی
|
||||
</Link>
|
||||
<button
|
||||
onClick={onB2BOpen}
|
||||
className="w-full text-right flex items-center gap-2 px-3 py-2 hover:bg-canina-blue/5 hover:text-canina-blue rounded-lg font-bold"
|
||||
|
||||
@ -185,16 +185,16 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
||||
<div className="min-h-screen bg-medical-gray-50 pt-2 pb-36 sm:pb-20 px-4 sm:px-6 lg:px-8 font-vazir" dir="rtl">
|
||||
<div className="max-w-7xl mx-auto w-full">
|
||||
{/* Standard Breadcrumb Navigation & Integrated Back Button */}
|
||||
<div className="flex items-center justify-between mb-8 pb-4 border-b border-medical-gray-200/60 font-vazir" dir="rtl">
|
||||
<div className="flex items-center gap-2 text-xs font-bold text-medical-gray-400 whitespace-nowrap">
|
||||
<span className="cursor-pointer hover:text-canina-blue transition-colors" onClick={() => router.push('/')}>خانه</span>
|
||||
<ChevronLeft className="w-3 h-3 text-medical-gray-300" />
|
||||
<span className="cursor-pointer hover:text-canina-blue transition-colors" onClick={() => router.push('/shop')}>فروشگاه</span>
|
||||
<ChevronLeft className="w-3 h-3 text-medical-gray-300" />
|
||||
<span className="text-canina-blue font-black">{product.nameFa || product.name}</span>
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4 mb-8 pb-4 border-b border-medical-gray-200/60 font-vazir" dir="rtl">
|
||||
<div className="flex items-center gap-2 text-xs font-bold text-medical-gray-400 overflow-x-auto py-1 sleek-hscroll">
|
||||
<span className="cursor-pointer hover:text-canina-blue transition-colors shrink-0" onClick={() => router.push('/')}>خانه</span>
|
||||
<ChevronLeft className="w-3 h-3 text-medical-gray-300 shrink-0" />
|
||||
<span className="cursor-pointer hover:text-canina-blue transition-colors shrink-0" onClick={() => router.push('/shop')}>فروشگاه</span>
|
||||
<ChevronLeft className="w-3 h-3 text-medical-gray-300 shrink-0" />
|
||||
<span className="text-canina-blue font-black truncate">{product.nameFa || product.name}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-end gap-2 shrink-0">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (typeof window !== 'undefined' && navigator.clipboard) {
|
||||
@ -202,14 +202,14 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
||||
toast.success("لینک مشخصات و دوز دارویی محصول کپی شد");
|
||||
}
|
||||
}}
|
||||
className="px-3.5 py-1.5 bg-canina-blue/5 border border-canina-blue/20 rounded-lg text-canina-blue shadow-xs flex items-center gap-1.5 font-bold text-xs hover:bg-canina-blue hover:text-white transition-all whitespace-nowrap"
|
||||
className="flex-1 sm:flex-none px-3.5 py-2 bg-canina-blue/5 border border-canina-blue/20 rounded-xl text-canina-blue shadow-xs flex items-center justify-center gap-1.5 font-bold text-xs hover:bg-canina-blue hover:text-white transition-all whitespace-nowrap"
|
||||
>
|
||||
<Share2 className="w-3.5 h-3.5" />
|
||||
<span>اشتراکگذاری دوز</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="px-3.5 py-1.5 bg-white border border-medical-gray-200 rounded-lg text-medical-gray-600 shadow-xs flex items-center gap-1.5 font-bold text-xs hover:border-canina-blue hover:text-canina-blue transition-all whitespace-nowrap"
|
||||
className="flex-1 sm:flex-none px-3.5 py-2 bg-white border border-medical-gray-200 rounded-xl text-medical-gray-600 shadow-xs flex items-center justify-center gap-1.5 font-bold text-xs hover:border-canina-blue hover:text-canina-blue transition-all whitespace-nowrap"
|
||||
>
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
<span>بازگشت</span>
|
||||
|
||||
@ -26,24 +26,13 @@ export default function SafeImage({
|
||||
if (!src || error) {
|
||||
return (
|
||||
<div className={cn(
|
||||
"flex flex-col items-center justify-center bg-white border border-medical-gray-100 p-4 text-center min-h-[120px] rounded-3xl shadow-inner relative overflow-hidden",
|
||||
"flex flex-col items-center justify-center bg-medical-gray-50/80 border border-medical-gray-100 p-1.5 text-center w-full h-full rounded-2xl relative overflow-hidden select-none",
|
||||
className
|
||||
)}>
|
||||
{/* Subtle Brand Pattern Background */}
|
||||
<div className="absolute inset-0 opacity-[0.03] pointer-events-none">
|
||||
<div className="grid grid-cols-6 h-full w-full">
|
||||
{Array.from({length: 24}).map((_, i) => (
|
||||
<div key={i} className="border border-canina-blue" />
|
||||
))}
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-canina-blue rounded-xl flex items-center justify-center text-white font-black text-base sm:text-lg shadow-sm italic shrink-0">
|
||||
C
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-16 h-16 bg-canina-blue rounded-2xl flex items-center justify-center text-white font-black text-3xl mb-4 shadow-xl shadow-canina-blue/20 italic transform -rotate-6">C</div>
|
||||
<div className="relative">
|
||||
<ImageOff className="w-5 h-5 text-medical-gray-200 mb-2" />
|
||||
<Sparkles className="w-3 h-3 text-canina-blue absolute -top-1 -right-1 animate-pulse" />
|
||||
</div>
|
||||
<span className="text-[9px] font-black text-canina-blue/60 uppercase tracking-[0.2em] font-vazir leading-relaxed">
|
||||
<span className="text-[8px] font-black text-canina-blue/70 font-vazir leading-tight mt-1 line-clamp-1">
|
||||
{fallbackText}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
114
frontend/application/components/SearchableSelect.tsx
Normal file
114
frontend/application/components/SearchableSelect.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { ChevronDown, Search, X } from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
interface SearchableSelectProps {
|
||||
id?: string;
|
||||
options: string[];
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
searchPlaceholder?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function SearchableSelect({
|
||||
id,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = "انتخاب کنید...",
|
||||
searchPlaceholder = "جستجو...",
|
||||
disabled = false,
|
||||
className
|
||||
}: SearchableSelectProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const filteredOptions = options.filter(opt =>
|
||||
opt.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={cn("relative w-full", className)}>
|
||||
<button
|
||||
id={id}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={cn(
|
||||
"w-full bg-white border border-medical-gray-200 rounded-2xl py-3 px-4 text-xs sm:text-sm font-bold flex items-center justify-between transition-all outline-none focus:ring-2 focus:ring-canina-blue/20",
|
||||
disabled && "bg-medical-gray-50 opacity-60 cursor-not-allowed",
|
||||
value ? "text-medical-gray-900" : "text-medical-gray-400"
|
||||
)}
|
||||
>
|
||||
<span className="truncate">{value || placeholder}</span>
|
||||
<ChevronDown className={cn("w-4 h-4 text-medical-gray-400 transition-transform", isOpen && "rotate-180")} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute z-50 top-full left-0 right-0 mt-1 bg-white border border-medical-gray-200 rounded-2xl shadow-xl overflow-hidden max-h-60 flex flex-col font-vazir animate-in fade-in slide-in-from-top-2 duration-150">
|
||||
<div className="p-2 border-b border-medical-gray-100 relative flex items-center">
|
||||
<Search className="w-3.5 h-3.5 text-medical-gray-400 absolute right-4" />
|
||||
<input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={searchPlaceholder}
|
||||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-1.5 pr-8 pl-3 text-xs font-bold text-medical-gray-900 outline-none focus:border-canina-blue"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="absolute left-4 text-medical-gray-400 hover:text-medical-gray-600"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="overflow-y-auto flex-1 p-1">
|
||||
{filteredOptions.length === 0 ? (
|
||||
<div className="p-3 text-center text-xs text-medical-gray-400 font-bold">
|
||||
موردی یافت نشد
|
||||
</div>
|
||||
) : (
|
||||
filteredOptions.map((opt) => (
|
||||
<button
|
||||
key={opt}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(opt);
|
||||
setIsOpen(false);
|
||||
setSearchQuery("");
|
||||
}}
|
||||
className={cn(
|
||||
"w-full text-right px-3 py-2 rounded-xl text-xs font-bold transition-colors flex items-center justify-between",
|
||||
value === opt ? "bg-canina-blue/10 text-canina-blue" : "hover:bg-medical-gray-50 text-medical-gray-700"
|
||||
)}
|
||||
>
|
||||
<span>{opt}</span>
|
||||
{value === opt && <span className="w-1.5 h-1.5 rounded-full bg-canina-blue" />}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user