feat: searchable province/city selects, zip code support, fix trust-seals header, responsive mobile logo & auth, cart scroll lock & safe image fallback, toast font
Some checks failed
Deploy Canina / deploy (push) Failing after 18s
Some checks failed
Deploy Canina / deploy (push) Failing after 18s
This commit is contained in:
parent
8e9ac246cb
commit
f8e9ebfc44
@ -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,44 +207,35 @@ 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,
|
||||
...formData,
|
||||
province: newProv,
|
||||
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"
|
||||
|
||||
@ -50,6 +50,18 @@ export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate
|
||||
setSuggestedProduct(null);
|
||||
}
|
||||
}, [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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@ -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>
|
||||
<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 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