Compare commits
No commits in common. "20a1b225c88f03ef473ebbcaecc699796e6fa23d" and "27ef5979aab276cbca4718587f1dd9cba6c6b774" have entirely different histories.
20a1b225c8
...
27ef5979aa
@ -9,155 +9,151 @@ import Link from 'next/link';
|
|||||||
import { toPersian } from "../lib/utils";
|
import { toPersian } from "../lib/utils";
|
||||||
import { useSettingsStore } from "../lib/store/settingsStore";
|
import { useSettingsStore } from "../lib/store/settingsStore";
|
||||||
import api from "../lib/services/api";
|
import api from "../lib/services/api";
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default function HomeClient() {
|
export default function HomeClient() {
|
||||||
const getText = useSettingsStore(state => state.getText);
|
const getText = useSettingsStore(state => state.getText);
|
||||||
const [data, setData] = useState<any>(null);
|
const [data, setData] = useState<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get('/home')
|
api.get('/home')
|
||||||
.then(res => setData(res.data))
|
.then(res => setData(res.data))
|
||||||
.catch(err => console.error('Home data fetch error:', err));
|
.catch(err => console.error('Home data fetch error:', err));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const banners = data?.heroBanners || [];
|
const banners = data?.heroBanners || [];
|
||||||
const testimonials = data?.vetTestimonials || [];
|
const testimonials = data?.vetTestimonials || [];
|
||||||
const rules = data?.smartAdvisorRules || [];
|
const rules = data?.smartAdvisorRules || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Hero banners={banners} />
|
<Hero banners={banners} />
|
||||||
<SmartAdvisor rules={rules} />
|
<SmartAdvisor rules={rules} />
|
||||||
<FeaturedProducts />
|
<FeaturedProducts />
|
||||||
<VetGallery testimonials={testimonials} />
|
<VetGallery testimonials={testimonials} />
|
||||||
|
|
||||||
{/* Navigation to Sections */}
|
{/* Navigation to Sections */}
|
||||||
<section className="py-12 bg-medical-gray-50 border-y border-medical-gray-100 font-sans">
|
<section className="py-12 bg-medical-gray-50 border-y border-medical-gray-100 font-sans">
|
||||||
<div className="max-w-7xl mx-auto px-4 grid md:grid-cols-2 gap-8">
|
<div className="max-w-7xl mx-auto px-4 grid md:grid-cols-2 gap-8">
|
||||||
{/* Specialty Shop Card */}
|
{/* Specialty Shop Card */}
|
||||||
<Link
|
<Link
|
||||||
href="/shop"
|
href="/shop"
|
||||||
className="bg-gradient-to-br from-blue-50/60 to-canina-blue/5 p-12 rounded-[2.5rem] border border-blue-100/60 shadow-md hover:shadow-2xl hover:-translate-y-2 transition-all cursor-pointer group block relative overflow-hidden"
|
className="bg-gradient-to-br from-blue-50/60 to-canina-blue/5 p-12 rounded-[2.5rem] border border-blue-100/60 shadow-md hover:shadow-2xl hover:-translate-y-2 transition-all cursor-pointer group block relative overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="absolute -top-12 -left-12 w-32 h-32 bg-canina-blue/5 rounded-full blur-2xl group-hover:scale-150 transition-transform duration-700" />
|
<div className="absolute -top-12 -left-12 w-32 h-32 bg-canina-blue/5 rounded-full blur-2xl group-hover:scale-150 transition-transform duration-700" />
|
||||||
<h3 className="text-3xl font-black text-medical-gray-900 mb-4 group-hover:text-canina-blue transition-colors font-lalezar tracking-wide">
|
<h3 className="text-3xl font-black text-medical-gray-900 mb-4 group-hover:text-canina-blue transition-colors font-lalezar tracking-wide">
|
||||||
{getText('nav_card_shop_title', "فروشگاه تخصصی")}
|
{getText('nav_card_shop_title', "فروشگاه تخصصی")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-medical-gray-600 mb-8 leading-relaxed font-shabnam">
|
<p className="text-medical-gray-600 mb-8 leading-relaxed font-shabnam">
|
||||||
{getText('nav_card_shop_desc', "دسترسی به تمامی محصولات بر اساس علائم بالینی و نیازهای پت شما.")}
|
{getText('nav_card_shop_desc', "دسترسی به تمامی محصولات بر اساس علائم بالینی و نیازهای پت شما.")}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2 text-canina-blue font-black tracking-widest uppercase text-xs font-vazir">
|
<div className="flex items-center gap-2 text-canina-blue font-black tracking-widest uppercase text-xs font-vazir">
|
||||||
{getText('nav_card_shop_cta', "کاتالوگ کامل محصولات")}
|
{getText('nav_card_shop_cta', "کاتالوگ کامل محصولات")}
|
||||||
<span className="text-lg group-hover:-translate-x-1 transition-transform">←</span>
|
<span className="text-lg group-hover:-translate-x-1 transition-transform">←</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Scientific Wiki Card */}
|
{/* Scientific Wiki Card */}
|
||||||
<Link
|
<Link
|
||||||
href="/wiki"
|
href="/wiki"
|
||||||
className="bg-gradient-to-br from-emerald-50/60 to-green-50/20 p-12 rounded-[2.5rem] border border-emerald-100/60 shadow-md hover:shadow-2xl hover:-translate-y-2 transition-all cursor-pointer group block relative overflow-hidden"
|
className="bg-gradient-to-br from-emerald-50/60 to-green-50/20 p-12 rounded-[2.5rem] border border-emerald-100/60 shadow-md hover:shadow-2xl hover:-translate-y-2 transition-all cursor-pointer group block relative overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="absolute -top-12 -left-12 w-32 h-32 bg-emerald-500/5 rounded-full blur-2xl group-hover:scale-150 transition-transform duration-700" />
|
<div className="absolute -top-12 -left-12 w-32 h-32 bg-emerald-500/5 rounded-full blur-2xl group-hover:scale-150 transition-transform duration-700" />
|
||||||
<h3 className="text-3xl font-black text-medical-gray-900 mb-4 group-hover:text-emerald-700 transition-colors font-lalezar tracking-wide">
|
<h3 className="text-3xl font-black text-medical-gray-900 mb-4 group-hover:text-emerald-700 transition-colors font-lalezar tracking-wide">
|
||||||
{getText('nav_card_wiki_title', "دانشنامه علمی")}
|
{getText('nav_card_wiki_title', "دانشنامه علمی")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-medical-gray-600 mb-8 leading-relaxed font-shabnam">
|
<p className="text-medical-gray-600 mb-8 leading-relaxed font-shabnam">
|
||||||
{getText('nav_card_wiki_desc', "آشنایی با ترکیبات نایاب و استانداردهای دارویی محصولات آلمانی کانینا.")}
|
{getText('nav_card_wiki_desc', "آشنایی با ترکیبات نایاب و استانداردهای دارویی محصولات آلمانی کانینا.")}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2 text-emerald-700 font-black tracking-widest uppercase text-xs font-vazir">
|
<div className="flex items-center gap-2 text-emerald-700 font-black tracking-widest uppercase text-xs font-vazir">
|
||||||
{getText('nav_card_wiki_cta', "مطالعه مقالات تخصصی")}
|
{getText('nav_card_wiki_cta', "مطالعه مقالات تخصصی")}
|
||||||
<span className="text-lg group-hover:-translate-x-1 transition-transform">←</span>
|
<span className="text-lg group-hover:-translate-x-1 transition-transform">←</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* About Section Teaser */}
|
{/* About Section Teaser */}
|
||||||
<section className="py-16 bg-white overflow-hidden font-sans">
|
<section className="py-16 bg-white overflow-hidden font-sans">
|
||||||
<div className="max-w-7xl mx-auto px-4 grid lg:grid-cols-2 gap-16 lg:gap-24 items-center">
|
<div className="max-w-7xl mx-auto px-4 grid lg:grid-cols-2 gap-16 lg:gap-24 items-center">
|
||||||
<div className="relative overflow-hidden rounded-[3rem]">
|
<div className="relative overflow-hidden rounded-[3rem]">
|
||||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-canina-blue/5 rounded-full blur-3xl" />
|
<div className="absolute -top-10 -right-10 w-40 h-40 bg-canina-blue/5 rounded-full blur-3xl" />
|
||||||
<Image
|
<img
|
||||||
src="/assets/images/regenerated_image_1779109861747.png"
|
src="/assets/images/regenerated_image_1779109861747.png"
|
||||||
alt="آزمایشگاه تخصصی تولید مکملهای دارویی کانینا آلمان"
|
alt="آزمایشگاه تخصصی تولید مکملهای دارویی کانینا آلمان"
|
||||||
className="rounded-[3rem] shadow-2xl relative z-10 grayscale-[10%] w-full"
|
className="rounded-[3rem] shadow-2xl relative z-10 grayscale-[10%] w-full"
|
||||||
width={640}
|
/>
|
||||||
height={425}
|
<div className="absolute bottom-4 left-4 sm:bottom-6 sm:left-6 bg-canina-blue text-white p-4 sm:p-6 md:p-8 rounded-[1.5rem] sm:rounded-[2rem] shadow-2xl z-20 max-w-[80%] border border-white/10">
|
||||||
loading="lazy"
|
<div className="text-2xl sm:text-4xl font-black mb-1 font-lalezar tracking-wide">{getText('quality', "کیفیت")}</div>
|
||||||
/>
|
<div className="text-[10px] sm:text-xs uppercase tracking-widest font-bold opacity-90 font-vazir">{getText('ultra_pharmaceutical', "استانداردهای فوقدارویی")}</div>
|
||||||
<div className="absolute bottom-4 left-4 sm:bottom-6 sm:left-6 bg-canina-blue text-white p-4 sm:p-6 md:p-8 rounded-[1.5rem] sm:rounded-[2rem] shadow-2xl z-20 max-w-[80%] border border-white/10">
|
</div>
|
||||||
<div className="text-2xl sm:text-4xl font-black mb-1 font-lalezar tracking-wide">{getText('quality', "کیفیت")}</div>
|
</div>
|
||||||
<div className="text-[10px] sm:text-xs uppercase tracking-widest font-bold opacity-90 font-vazir">{getText('ultra_pharmaceutical', "استانداردهای فوقدارویی")}</div>
|
<div>
|
||||||
</div>
|
<div className="text-canina-blue text-xs font-bold uppercase tracking-[0.2em] mb-4 font-vazir">
|
||||||
</div>
|
{getText('about_label', "میراث ما از آلمان")}
|
||||||
<div>
|
</div>
|
||||||
<div className="text-canina-blue text-xs font-bold uppercase tracking-[0.2em] mb-4 font-vazir">
|
<h2 className="text-3xl lg:text-5xl font-black text-medical-gray-900 leading-[1.2] mb-12 font-lalezar tracking-wide">
|
||||||
{getText('about_label', "میراث ما از آلمان")}
|
{getText('about_title', "چرا برند آلمانی Canina مرجع دامپزشکان است؟")}
|
||||||
</div>
|
</h2>
|
||||||
<h2 className="text-3xl lg:text-5xl font-black text-medical-gray-900 leading-[1.2] mb-12 font-lalezar tracking-wide">
|
<div className="space-y-6">
|
||||||
{getText('about_title', "چرا برند آلمانی Canina مرجع دامپزشکان است؟")}
|
{[
|
||||||
</h2>
|
{ title: getText('about_item1_title', "مواد اولیه نایاب"), desc: getText('about_item1_desc', "استفاده از پودر صدف لبسبز اصل نیوزیلند و مواد ارگانیک با گرید دارویی.") },
|
||||||
<div className="space-y-6">
|
{ title: getText('about_item2_title', "فاقد مواد نگهدارنده"), desc: getText('about_item2_desc', "تمامی محصولات ۱۰۰٪ طبیعی و فاقد رنگهای مصنوعی و طعمدهندههای شیمیایی هستند.") },
|
||||||
{[
|
{ title: getText('about_item3_title', "تاییدیه اروپا"), desc: getText('about_item3_desc', "مطابق با سختگیرانهترین استانداردهای ایمنی مواد غذایی و دارویی در اتحادیه اروپا.") }
|
||||||
{ title: getText('about_item1_title', "مواد اولیه نایاب"), desc: getText('about_item1_desc', "استفاده از پودر صدف لبسبز اصل نیوزیلند و مواد ارگانیک با گرید دارویی.") },
|
].map((item, idx) => (
|
||||||
{ title: getText('about_item2_title', "فاقد مواد نگهدارنده"), desc: getText('about_item2_desc', "تمامی محصولات ۱۰۰٪ طبیعی و فاقد رنگهای مصنوعی و طعمدهندههای شیمیایی هستند.") },
|
<div key={idx} className="flex gap-4 items-center">
|
||||||
{ title: getText('about_item3_title', "تاییدیه اروپا"), desc: getText('about_item3_desc', "مطابق با سختگیرانهترین استانداردهای ایمنی مواد غذایی و دارویی در اتحادیه اروپا.") }
|
<div className="w-12 h-12 rounded-2xl bg-medical-gray-50 flex items-center justify-center flex-shrink-0 text-canina-blue font-black font-lalezar text-xl shadow-sm">
|
||||||
].map((item, idx) => (
|
{toPersian(idx + 1)}
|
||||||
<div key={idx} className="flex gap-4 items-center">
|
</div>
|
||||||
<div className="w-12 h-12 rounded-2xl bg-medical-gray-50 flex items-center justify-center flex-shrink-0 text-canina-blue font-black font-lalezar text-xl shadow-sm">
|
<div>
|
||||||
{toPersian(idx + 1)}
|
<h4 className="text-lg font-bold text-medical-gray-900 mb-1 font-shabnam">{item.title}</h4>
|
||||||
</div>
|
<p className="text-sm text-medical-gray-500 leading-relaxed font-medium font-sans">{item.desc}</p>
|
||||||
<div>
|
{idx === 0 && (
|
||||||
<h4 className="text-lg font-bold text-medical-gray-900 mb-1 font-shabnam">{item.title}</h4>
|
<span className="text-[10px] text-slate-400 mt-1 block">
|
||||||
<p className="text-sm text-medical-gray-500 leading-relaxed font-medium font-sans">{item.desc}</p>
|
* تهیه شده با بالاترین استانداردهای دارویی انسان - کاملاً بیخطر در صورت لیسیده شدن توسط حیوان.
|
||||||
{idx === 0 && (
|
</span>
|
||||||
<span className="text-[10px] text-slate-400 mt-1 block">
|
)}
|
||||||
* تهیه شده با بالاترین استانداردهای دارویی انسان - کاملاً بیخطر در صورت لیسیده شدن توسط حیوان.
|
{idx === 2 && (
|
||||||
</span>
|
<span className="text-[10px] text-slate-400 mt-1 block">
|
||||||
)}
|
* تاییدیه رسمی ایمنی مواد غذایی و دارویی (HACCP & IFS) از اتحادیه اروپا.
|
||||||
{idx === 2 && (
|
</span>
|
||||||
<span className="text-[10px] text-slate-400 mt-1 block">
|
)}
|
||||||
* تاییدیه رسمی ایمنی مواد غذایی و دارویی (HACCP & IFS) از اتحادیه اروپا.
|
</div>
|
||||||
</span>
|
</div>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Call to Action */}
|
{/* Call to Action */}
|
||||||
<section className="bg-canina-blue py-12 relative overflow-hidden font-sans">
|
<section className="bg-canina-blue py-12 relative overflow-hidden font-sans">
|
||||||
<div className="absolute inset-0 opacity-10">
|
<div className="absolute inset-0 opacity-10">
|
||||||
<div className="grid grid-cols-10 h-full w-full">
|
<div className="grid grid-cols-10 h-full w-full">
|
||||||
{Array.from({ length: 100 }).map((_, i) => (
|
{Array.from({length: 100}).map((_, i) => (
|
||||||
<div key={i} className="border border-white/20" />
|
<div key={i} className="border border-white/20" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-4xl mx-auto px-4 text-center relative z-10">
|
<div className="max-w-4xl mx-auto px-4 text-center relative z-10">
|
||||||
<h2 className="text-3xl lg:text-5xl font-black text-white mb-8 leading-tight font-lalezar tracking-wide">
|
<h2 className="text-3xl lg:text-5xl font-black text-white mb-8 leading-tight font-lalezar tracking-wide">
|
||||||
{getText('cta_title', "میخواهید بدانید کدام محصول برای پت شما مناسبتر است؟")}
|
{getText('cta_title', "میخواهید بدانید کدام محصول برای پت شما مناسبتر است؟")}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-white/80 text-lg mb-10 leading-relaxed font-shabnam">
|
<p className="text-white/80 text-lg mb-10 leading-relaxed font-shabnam">
|
||||||
{getText('cta_desc', "تیم متخصص دامپزشکی کانینا ایران آماده پاسخگویی به سوالات شماست.")}
|
{getText('cta_desc', "تیم متخصص دامپزشکی کانینا ایران آماده پاسخگویی به سوالات شماست.")}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap justify-center gap-4">
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
<button className="bg-white text-canina-blue px-12 py-5 rounded-full font-black text-xl hover:scale-105 transition-transform shadow-2xl shadow-black/20 font-shabnam">
|
<button className="bg-white text-canina-blue px-12 py-5 rounded-full font-black text-xl hover:scale-105 transition-transform shadow-2xl shadow-black/20 font-shabnam">
|
||||||
{getText('cta_btn_primary', "دریافت رژیم مکمل رایگان")}
|
{getText('cta_btn_primary', "دریافت رژیم مکمل رایگان")}
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
href="/shop"
|
href="/shop"
|
||||||
className="bg-transparent border-2 border-white text-white px-12 py-5 rounded-full font-black text-xl hover:bg-white hover:text-canina-blue transition-all font-shabnam"
|
className="bg-transparent border-2 border-white text-white px-12 py-5 rounded-full font-black text-xl hover:bg-white hover:text-canina-blue transition-all font-shabnam"
|
||||||
>
|
>
|
||||||
{getText('cta_btn_secondary', "پروانه دامپزشکی و اصالت کالا")}
|
{getText('cta_btn_secondary', "پروانه دامپزشکی و اصالت کالا")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -366,37 +366,35 @@ export default function ArchivePage({
|
|||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
|
|
||||||
{/* Standard Breadcrumb Navigation & Integrated Back Button */}
|
{/* 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 gap-2">
|
<div className="flex items-center justify-between mb-8 pb-4 border-b border-medical-gray-200/60 font-vazir">
|
||||||
{/* Back Button - always visible */}
|
<div className="flex items-center gap-2 text-xs font-bold text-medical-gray-400 whitespace-nowrap">
|
||||||
<button
|
<span className="cursor-pointer hover:text-canina-blue transition-colors" onClick={() => router.push('/')}>خانه</span>
|
||||||
onClick={() => router.push('/')}
|
<ChevronLeft className="w-3 h-3 text-medical-gray-300" />
|
||||||
className="flex-shrink-0 px-3 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"
|
<span className="cursor-pointer hover:text-canina-blue transition-colors" onClick={() => { setSelectedCategory("all"); }}>کاتالوگ</span>
|
||||||
>
|
|
||||||
<ChevronRight className="w-3.5 h-3.5" />
|
|
||||||
<span>بازگشت</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Breadcrumb - truncates on mobile */}
|
|
||||||
<div className="flex items-center gap-1.5 text-xs font-bold text-medical-gray-400 overflow-hidden min-w-0">
|
|
||||||
<span className="hidden sm:inline cursor-pointer hover:text-canina-blue transition-colors shrink-0" onClick={() => router.push('/')}>خانه</span>
|
|
||||||
<ChevronLeft className="hidden sm:block w-3 h-3 text-medical-gray-300 shrink-0" />
|
|
||||||
<span className="cursor-pointer hover:text-canina-blue transition-colors shrink-0" onClick={() => { setSelectedCategory("all"); }}>کاتالوگ</span>
|
|
||||||
{selectedCategory !== "all" && (
|
{selectedCategory !== "all" && (
|
||||||
<>
|
<>
|
||||||
<ChevronLeft className="w-3 h-3 text-medical-gray-300 shrink-0" />
|
<ChevronLeft className="w-3 h-3 text-medical-gray-300" />
|
||||||
<span className="text-canina-blue font-black truncate">{CATEGORY_LABELS[selectedCategory] || selectedCategory}</span>
|
<span className="text-canina-blue font-black">{CATEGORY_LABELS[selectedCategory] || selectedCategory}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Filter Button */}
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsMobileFilterOpen(!isMobileFilterOpen)}
|
onClick={() => setIsMobileFilterOpen(!isMobileFilterOpen)}
|
||||||
className="lg:hidden flex-shrink-0 flex items-center gap-1.5 bg-canina-blue text-white px-3 py-1.5 rounded-lg text-xs font-bold shadow-xs"
|
className="lg:hidden flex items-center gap-1.5 bg-canina-blue text-white px-3 py-1.5 rounded-lg text-xs font-bold shadow-xs"
|
||||||
>
|
>
|
||||||
<Filter className="w-3.5 h-3.5" />
|
<Filter className="w-3.5 h-3.5" />
|
||||||
<span>فیلترها {activeSymptoms.length > 0 || selectedPet !== "all" || selectedCategory !== "all" ? "•" : ""}</span>
|
<span>فیلترها {activeSymptoms.length > 0 || selectedPet !== "all" || selectedCategory !== "all" ? "•" : ""}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push('/')}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-3.5 h-3.5" />
|
||||||
|
<span>بازگشت</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row gap-8">
|
<div className="flex flex-col lg:flex-row gap-8">
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { motion, AnimatePresence } from "motion/react";
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
import { X, User, Building2, Heart, ShieldCheck, Phone, Key, LogIn, ArrowRight, RefreshCw, Lock } from "lucide-react";
|
import { X, User, Building2, Heart, ShieldCheck, Phone, Key, LogIn, ArrowRight, RefreshCw } from "lucide-react";
|
||||||
import { useUserStore, UserRole } from "../lib/store/userStore";
|
import { useUserStore, UserRole } from "../lib/store/userStore";
|
||||||
import { authService } from "../lib/services/authService";
|
import { authService } from "../lib/services/authService";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -14,10 +14,9 @@ interface AuthModalProps {
|
|||||||
|
|
||||||
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
||||||
const { setRole, setLoggedIn, fetchProfile } = useUserStore();
|
const { setRole, setLoggedIn, fetchProfile } = useUserStore();
|
||||||
const [view, setView] = useState<"login" | "register" | "otp-phone" | "otp" | "forgot-password" | "forgot-otp" | "reset-password">("login");
|
const [view, setView] = useState<"login" | "register" | "otp-phone" | "otp">("login");
|
||||||
const [phoneNumber, setPhoneNumber] = useState("");
|
const [phoneNumber, setPhoneNumber] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [newPassword, setNewPassword] = useState("");
|
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [firstName, setFirstName] = useState("");
|
const [firstName, setFirstName] = useState("");
|
||||||
const [lastName, setLastName] = useState("");
|
const [lastName, setLastName] = useState("");
|
||||||
@ -41,7 +40,6 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
setView("login");
|
setView("login");
|
||||||
setPhoneNumber("");
|
setPhoneNumber("");
|
||||||
setPassword("");
|
setPassword("");
|
||||||
setNewPassword("");
|
|
||||||
setEmail("");
|
setEmail("");
|
||||||
setFirstName("");
|
setFirstName("");
|
||||||
setLastName("");
|
setLastName("");
|
||||||
@ -161,67 +159,6 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleForgotSendOtp = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const cleanPhone = phoneNumber.trim();
|
|
||||||
if (!/^09\d{9}$/.test(cleanPhone)) {
|
|
||||||
toast.error("شماره موبایل نامعتبر است");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await authService.sendOtp(cleanPhone);
|
|
||||||
if ((res as any).code) {
|
|
||||||
toast.info(`کد تایید (تست): ${(res as any).code}`, { duration: 10000 });
|
|
||||||
} else {
|
|
||||||
toast.success("کد بازیابی رمز پیامک شد");
|
|
||||||
}
|
|
||||||
setView("forgot-otp");
|
|
||||||
setCountdown(120);
|
|
||||||
} catch (err: any) {
|
|
||||||
toast.error(err.message || "خطا در ارسال کد");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleForgotVerifyOtp = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (otpCode.trim().length !== 5) {
|
|
||||||
toast.error("کد باید ۵ رقم باشد");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
// Verify OTP - if success, allow password reset
|
|
||||||
await authService.verifyOtp(phoneNumber.trim(), otpCode.trim());
|
|
||||||
setView("reset-password");
|
|
||||||
} catch (err: any) {
|
|
||||||
toast.error(err.message || "کد تایید نامعتبر است");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetPassword = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (newPassword.length < 6) {
|
|
||||||
toast.error("رمز جدید باید حداقل ۶ کاراکتر باشد");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
// Use authService to update password after OTP verification
|
|
||||||
await authService.updateProfile({ password: newPassword } as any);
|
|
||||||
toast.success("رمز عبور با موفقیت تغییر یافت");
|
|
||||||
setView("login");
|
|
||||||
} catch (err: any) {
|
|
||||||
toast.error(err.message || "خطا در تغییر رمز");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
@ -282,13 +219,10 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="login-phone" className="text-xs font-bold text-medical-gray-500">شماره موبایل</label>
|
<label className="text-xs font-bold text-medical-gray-500">شماره موبایل</label>
|
||||||
<input
|
<input
|
||||||
id="login-phone"
|
|
||||||
name="username"
|
|
||||||
type="tel"
|
type="tel"
|
||||||
maxLength={11}
|
maxLength={11}
|
||||||
autoComplete="username"
|
|
||||||
placeholder="مثال: ۰۹۱۲۳۴۵۶۷۸۹"
|
placeholder="مثال: ۰۹۱۲۳۴۵۶۷۸۹"
|
||||||
value={phoneNumber}
|
value={phoneNumber}
|
||||||
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ''))}
|
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ''))}
|
||||||
@ -297,12 +231,9 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="login-password" className="text-xs font-bold text-medical-gray-500">رمز عبور</label>
|
<label className="text-xs font-bold text-medical-gray-500">رمز عبور</label>
|
||||||
<input
|
<input
|
||||||
id="login-password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
|
||||||
placeholder="رمز عبور خود را وارد کنید"
|
placeholder="رمز عبور خود را وارد کنید"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
@ -311,7 +242,7 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between pt-2 flex-wrap gap-2">
|
<div className="flex items-center justify-between pt-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setView("otp-phone")}
|
onClick={() => setView("otp-phone")}
|
||||||
@ -319,22 +250,13 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
>
|
>
|
||||||
ورود با کد یکبار مصرف (OTP)
|
ورود با کد یکبار مصرف (OTP)
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-3">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={() => setView("register")}
|
||||||
onClick={() => { setPhoneNumber(""); setView("forgot-password"); }}
|
className="text-xs font-bold text-canina-blue hover:underline"
|
||||||
className="text-xs font-bold text-medical-gray-400 hover:text-canina-blue hover:underline"
|
>
|
||||||
>
|
حساب ندارید؟ ثبتنام
|
||||||
فراموشی رمز
|
</button>
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setView("register")}
|
|
||||||
className="text-xs font-bold text-canina-blue hover:underline"
|
|
||||||
>
|
|
||||||
حساب ندارید؟ ثبتنام
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@ -358,12 +280,9 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
>
|
>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label htmlFor="reg-fname" className="text-xs font-bold text-medical-gray-500">نام</label>
|
<label className="text-xs font-bold text-medical-gray-500">نام</label>
|
||||||
<input
|
<input
|
||||||
id="reg-fname"
|
|
||||||
name="given-name"
|
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="given-name"
|
|
||||||
value={firstName}
|
value={firstName}
|
||||||
onChange={(e) => setFirstName(e.target.value)}
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -371,12 +290,9 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label htmlFor="reg-lname" className="text-xs font-bold text-medical-gray-500">نام خانوادگی</label>
|
<label className="text-xs font-bold text-medical-gray-500">نام خانوادگی</label>
|
||||||
<input
|
<input
|
||||||
id="reg-lname"
|
|
||||||
name="family-name"
|
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="family-name"
|
|
||||||
value={lastName}
|
value={lastName}
|
||||||
onChange={(e) => setLastName(e.target.value)}
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -386,13 +302,10 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label htmlFor="reg-phone" className="text-xs font-bold text-medical-gray-500">شماره موبایل *</label>
|
<label className="text-xs font-bold text-medical-gray-500">شماره موبایل *</label>
|
||||||
<input
|
<input
|
||||||
id="reg-phone"
|
|
||||||
name="username"
|
|
||||||
type="tel"
|
type="tel"
|
||||||
maxLength={11}
|
maxLength={11}
|
||||||
autoComplete="username"
|
|
||||||
value={phoneNumber}
|
value={phoneNumber}
|
||||||
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ''))}
|
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ''))}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -401,12 +314,9 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label htmlFor="reg-email" className="text-xs font-bold text-medical-gray-500">ایمیل (اختیاری)</label>
|
<label className="text-xs font-bold text-medical-gray-500">ایمیل (اختیاری)</label>
|
||||||
<input
|
<input
|
||||||
id="reg-email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
type="email"
|
||||||
autoComplete="email"
|
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -415,12 +325,9 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label htmlFor="reg-password" className="text-xs font-bold text-medical-gray-500">رمز عبور *</label>
|
<label className="text-xs font-bold text-medical-gray-500">رمز عبور *</label>
|
||||||
<input
|
<input
|
||||||
id="reg-password"
|
|
||||||
name="new-password"
|
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="new-password"
|
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -570,119 +477,6 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
</button>
|
</button>
|
||||||
</motion.form>
|
</motion.form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Forgot Password - Enter Phone */}
|
|
||||||
{view === "forgot-password" && (
|
|
||||||
<motion.form
|
|
||||||
key="forgot-password-view"
|
|
||||||
initial={{ opacity: 0, x: 20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: -20 }}
|
|
||||||
onSubmit={handleForgotSendOtp}
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-4 text-center">
|
|
||||||
<Lock className="w-8 h-8 text-amber-500 mx-auto mb-2" />
|
|
||||||
<p className="text-sm font-bold text-amber-700">برای بازیابی رمز عبور، کد تایید به شمارهتان ارسال میشود</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="forgot-phone" className="text-xs font-bold text-medical-gray-500">شماره موبایل</label>
|
|
||||||
<input
|
|
||||||
id="forgot-phone"
|
|
||||||
name="username"
|
|
||||||
type="tel"
|
|
||||||
maxLength={11}
|
|
||||||
autoComplete="username"
|
|
||||||
placeholder="مثال: ۰۹۱۲۳۴۵۶۷۸۹"
|
|
||||||
value={phoneNumber}
|
|
||||||
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ''))}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 px-4 focus:ring-2 focus:ring-canina-blue/20 outline-none font-bold text-left tracking-widest"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading || phoneNumber.length < 11}
|
|
||||||
className="w-full py-4 bg-canina-blue text-white rounded-xl font-black hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-lg shadow-canina-blue/20 flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
{isLoading ? <RefreshCw className="w-5 h-5 animate-spin" /> : <>ارسال کد بازیابی</>}
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => setView("login")} className="w-full text-xs font-bold text-medical-gray-400 hover:text-canina-blue">بازگشت به ورود</button>
|
|
||||||
</motion.form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Forgot Password - Enter OTP */}
|
|
||||||
{view === "forgot-otp" && (
|
|
||||||
<motion.form
|
|
||||||
key="forgot-otp-view"
|
|
||||||
initial={{ opacity: 0, x: 20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: -20 }}
|
|
||||||
onSubmit={handleForgotVerifyOtp}
|
|
||||||
className="space-y-5"
|
|
||||||
>
|
|
||||||
<p className="text-sm font-bold text-medical-gray-600 text-center">کد ارسال شده به {toPersian(phoneNumber)} را وارد کنید</p>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
ref={otpInputRef}
|
|
||||||
id="forgot-otp-code"
|
|
||||||
name="one-time-code"
|
|
||||||
type="text"
|
|
||||||
autoComplete="one-time-code"
|
|
||||||
inputMode="numeric"
|
|
||||||
maxLength={5}
|
|
||||||
value={otpCode}
|
|
||||||
onChange={(e) => setOtpCode(e.target.value.replace(/[^0-9]/g, ''))}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 pl-4 pr-4 focus:ring-2 focus:ring-canina-blue/20 outline-none font-black text-2xl text-center tracking-[0.5em]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{countdown > 0 ? (
|
|
||||||
<p className="text-xs font-bold text-medical-gray-400 text-center">ارسال مجدد پس از {toPersian(countdown)} ثانیه</p>
|
|
||||||
) : (
|
|
||||||
<button type="button" onClick={handleForgotSendOtp as any} disabled={isLoading} className="text-xs font-black text-canina-blue hover:underline flex items-center gap-1.5 justify-center mx-auto">
|
|
||||||
<RefreshCw className="w-3.5 h-3.5" />ارسال مجدد
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button type="submit" disabled={isLoading || otpCode.length < 5} className="w-full py-4 bg-canina-blue text-white rounded-xl font-black disabled:opacity-50 transition-all shadow-lg shadow-canina-blue/20 flex items-center justify-center gap-2">
|
|
||||||
{isLoading ? <RefreshCw className="w-5 h-5 animate-spin" /> : <>تایید کد</>}
|
|
||||||
</button>
|
|
||||||
</motion.form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Reset Password */}
|
|
||||||
{view === "reset-password" && (
|
|
||||||
<motion.form
|
|
||||||
key="reset-password-view"
|
|
||||||
initial={{ opacity: 0, x: 20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: -20 }}
|
|
||||||
onSubmit={handleResetPassword}
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<div className="bg-green-50 border border-green-200 rounded-2xl p-4 text-center">
|
|
||||||
<p className="text-sm font-bold text-green-700">هویت تایید شد. رمز جدید خود را وارد کنید</p>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="new-password" className="text-xs font-bold text-medical-gray-500">رمز جدید</label>
|
|
||||||
<input
|
|
||||||
id="new-password"
|
|
||||||
name="new-password"
|
|
||||||
type="password"
|
|
||||||
autoComplete="new-password"
|
|
||||||
placeholder="حداقل ۶ کاراکتر"
|
|
||||||
value={newPassword}
|
|
||||||
onChange={(e) => setNewPassword(e.target.value)}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 px-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-left"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button type="submit" disabled={isLoading || newPassword.length < 6} className="w-full py-4 bg-canina-blue text-white rounded-xl font-black disabled:opacity-50 transition-all shadow-lg shadow-canina-blue/20 flex items-center justify-center gap-2">
|
|
||||||
{isLoading ? <RefreshCw className="w-5 h-5 animate-spin" /> : <>تغییر رمز عبور</>}
|
|
||||||
</button>
|
|
||||||
</motion.form>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -36,27 +36,9 @@ export default function CheckoutPage() {
|
|||||||
const [dbProducts, setDbProducts] = useState<Product[]>([]);
|
const [dbProducts, setDbProducts] = useState<Product[]>([]);
|
||||||
const [paymentMethod, setPaymentMethod] = useState<string>('online');
|
const [paymentMethod, setPaymentMethod] = useState<string>('online');
|
||||||
|
|
||||||
const [selectedAddressId, setSelectedAddressId] = useState<string>('');
|
|
||||||
const [showNewAddressForm, setShowNewAddressForm] = useState(false);
|
|
||||||
|
|
||||||
// Custom manual address state if guest or not using stored addresses
|
|
||||||
const [manualName, setManualName] = useState('');
|
|
||||||
const [manualPhone, setManualPhone] = useState('');
|
|
||||||
const [manualAddress, setManualAddress] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
productService.getProducts({ limit: 999 }).then(res => setDbProducts(res.data));
|
productService.getProducts({ limit: 999 }).then(res => setDbProducts(res.data));
|
||||||
|
}, []);
|
||||||
// Set default selected address if logged in
|
|
||||||
if (profile.addresses && profile.addresses.length > 0) {
|
|
||||||
const def = profile.addresses.find(a => a.isDefault) || profile.addresses[0];
|
|
||||||
setSelectedAddressId(def.id);
|
|
||||||
} else if (profile.firstName) {
|
|
||||||
// Pre-fill profile info for manual fields
|
|
||||||
setManualName(`${profile.firstName} ${profile.lastName}`.trim());
|
|
||||||
setManualPhone(profile.mobile || '');
|
|
||||||
}
|
|
||||||
}, [profile]);
|
|
||||||
|
|
||||||
const subtotal = getSubtotal();
|
const subtotal = getSubtotal();
|
||||||
const roundedAmount = Math.ceil(subtotal / 10000) * 10000;
|
const roundedAmount = Math.ceil(subtotal / 10000) * 10000;
|
||||||
@ -73,49 +55,26 @@ export default function CheckoutPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFinalize = async () => {
|
const handleFinalize = async () => {
|
||||||
const totalAmount = getTotal();
|
|
||||||
|
|
||||||
if (paymentMethod === 'wallet' && profile.walletBalance < totalAmount) {
|
|
||||||
toast.error("اعتبار کیف پول شما کافی نیست. لطفاً ابتدا آن را شارژ کنید یا روش پرداخت دیگری انتخاب کنید.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const activePet = getActivePet();
|
const activePet = getActivePet();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. If using wallet, deduct balance and add purchase transaction
|
// 1. Register the order in the backend
|
||||||
if (paymentMethod === 'wallet') {
|
|
||||||
const newBalance = profile.walletBalance - totalAmount;
|
|
||||||
const newTransaction = {
|
|
||||||
id: `TR-${Math.floor(Math.random() * 90000) + 10000}`,
|
|
||||||
type: 'purchase' as const,
|
|
||||||
amount: totalAmount,
|
|
||||||
date: new Date().toISOString(),
|
|
||||||
status: 'success' as const
|
|
||||||
};
|
|
||||||
await updateProfile({
|
|
||||||
walletBalance: newBalance,
|
|
||||||
transactions: [newTransaction, ...(profile.transactions || [])]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Register the order in the backend
|
|
||||||
const orderId = await addOrder({
|
const orderId = await addOrder({
|
||||||
items: [...items],
|
items: [...items],
|
||||||
total: totalAmount,
|
total: getTotal(),
|
||||||
charityDonation: charityDonation,
|
charityDonation: charityDonation,
|
||||||
petId: activePet?.id
|
petId: activePet?.id
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Update user charity total
|
// 2. Update user charity total
|
||||||
if (charityDonation > 0) {
|
if (charityDonation > 0) {
|
||||||
await updateProfile({
|
updateProfile({
|
||||||
charityDonationTotal: (profile.charityDonationTotal || 0) + charityDonation
|
charityDonationTotal: (profile.charityDonationTotal || 0) + charityDonation
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Update pet consumptions for the refill logic
|
// 3. Update pet consumptions for the refill logic
|
||||||
if (activePet) {
|
if (activePet) {
|
||||||
const newConsumptions = [...(activePet.consumptions || [])];
|
const newConsumptions = [...(activePet.consumptions || [])];
|
||||||
|
|
||||||
@ -199,99 +158,20 @@ export default function CheckoutPage() {
|
|||||||
<h2 className="text-xl sm:text-2xl font-black text-medical-gray-900 italic">اطلاعات ارسال</h2>
|
<h2 className="text-xl sm:text-2xl font-black text-medical-gray-900 italic">اطلاعات ارسال</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{profile.addresses && profile.addresses.length > 0 && !showNewAddressForm ? (
|
<div className="grid md:grid-cols-2 gap-4 sm:gap-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-2">
|
||||||
<p className="text-xs font-bold text-medical-gray-500 mb-2">انتخاب از بین آدرسهای ذخیرهشده شما:</p>
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">نام و نام خانوادگی</label>
|
||||||
<div className="grid gap-3">
|
<input type="text" 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" placeholder="مثلاً: پارسا آقایی" />
|
||||||
{profile.addresses.map((addr) => (
|
</div>
|
||||||
<div
|
<div className="space-y-2">
|
||||||
key={addr.id}
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">شماره تماس</label>
|
||||||
onClick={() => setSelectedAddressId(addr.id)}
|
<input type="text" 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" placeholder="۰۹۱۲XXXXXXX" />
|
||||||
className={cn(
|
</div>
|
||||||
"p-4 rounded-2xl border-2 cursor-pointer transition-all flex items-start gap-3",
|
<div className="md:col-span-2 space-y-2">
|
||||||
selectedAddressId === addr.id ? "border-canina-blue bg-canina-blue/5" : "border-medical-gray-100 hover:border-medical-gray-200"
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">آدرس دقیق پستی</label>
|
||||||
)}
|
<textarea 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 h-28 sm:h-32" placeholder="استان، شهر، خیابان..." />
|
||||||
>
|
</div>
|
||||||
<input
|
</div>
|
||||||
type="radio"
|
|
||||||
name="selectedAddress"
|
|
||||||
checked={selectedAddressId === addr.id}
|
|
||||||
onChange={() => setSelectedAddressId(addr.id)}
|
|
||||||
className="mt-1"
|
|
||||||
/>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<span className="font-black text-sm text-medical-gray-900">{addr.title}</span>
|
|
||||||
{addr.isDefault && (
|
|
||||||
<span className="bg-canina-blue/10 text-canina-blue text-[9px] font-black px-2 py-0.5 rounded-full">پیشفرض</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-medical-gray-600 leading-relaxed truncate">{addr.province}، {addr.city}، {addr.detail}</p>
|
|
||||||
<p className="text-[10px] text-medical-gray-400 font-bold mt-1">گیرنده: {addr.receptorName} | همراه: {toPersian(addr.phone)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowNewAddressForm(true)}
|
|
||||||
className="mt-4 flex items-center gap-2 text-canina-blue font-black text-xs hover:underline"
|
|
||||||
>
|
|
||||||
<PlusCircle className="w-4 h-4" />
|
|
||||||
<span>استفاده از آدرس جدید</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{profile.addresses && profile.addresses.length > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={() => setShowNewAddressForm(false)}
|
|
||||||
className="mb-4 text-xs font-bold text-canina-blue hover:underline"
|
|
||||||
>
|
|
||||||
← بازگشت به آدرسهای ذخیرهشده
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div className="grid md:grid-cols-2 gap-4 sm:gap-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="chk-fullname" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">نام و نام خانوادگی</label>
|
|
||||||
<input
|
|
||||||
id="chk-fullname"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
autoComplete="name"
|
|
||||||
value={manualName}
|
|
||||||
onChange={(e) => setManualName(e.target.value)}
|
|
||||||
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"
|
|
||||||
placeholder="مثلاً: پارسا آقایی"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="chk-tel" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">شماره تماس</label>
|
|
||||||
<input
|
|
||||||
id="chk-tel"
|
|
||||||
name="tel"
|
|
||||||
type="tel"
|
|
||||||
autoComplete="tel"
|
|
||||||
value={manualPhone}
|
|
||||||
onChange={(e) => setManualPhone(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"
|
|
||||||
placeholder="۰۹۱۲XXXXXXX"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="md:col-span-2 space-y-2">
|
|
||||||
<label htmlFor="chk-street" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">آدرس دقیق پستی</label>
|
|
||||||
<textarea
|
|
||||||
id="chk-street"
|
|
||||||
name="street-address"
|
|
||||||
autoComplete="street-address"
|
|
||||||
value={manualAddress}
|
|
||||||
onChange={(e) => setManualAddress(e.target.value)}
|
|
||||||
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 h-28 sm:h-32"
|
|
||||||
placeholder="استان، شهر، خیابان..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Step 2: Payment */}
|
{/* Step 2: Payment */}
|
||||||
@ -378,46 +258,46 @@ export default function CheckoutPage() {
|
|||||||
{/* Sidebar Summary */}
|
{/* Sidebar Summary */}
|
||||||
<div className="lg:col-span-4 space-y-8">
|
<div className="lg:col-span-4 space-y-8">
|
||||||
<div className="bg-medical-gray-900 rounded-[3rem] p-10 text-white shadow-2xl sticky top-32">
|
<div className="bg-medical-gray-900 rounded-[3rem] p-10 text-white shadow-2xl sticky top-32">
|
||||||
<h3 className="text-2xl font-black italic mb-8 border-b border-white/20 pb-4 text-white">خلاصه سفارش</h3>
|
<h3 className="text-2xl font-black italic mb-8 border-b border-white/10 pb-4">خلاصه سفارش</h3>
|
||||||
|
|
||||||
<div className="space-y-6 mb-10 overflow-y-auto max-h-60 pr-2">
|
<div className="space-y-6 mb-10 overflow-y-auto max-h-60 pr-2">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div key={item.product.id} className="flex justify-between items-start gap-4">
|
<div key={item.product.id} className="flex justify-between items-start gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-black text-white leading-tight">{item.product.name}</p>
|
<p className="text-sm font-black text-white/90 leading-tight">{item.product.name}</p>
|
||||||
<p className="text-[10px] text-white/60 font-bold mt-1">تعداد: {toPersian(item.quantity)} بسته</p>
|
<p className="text-[10px] text-white/40 font-bold mt-1">تعداد: {toPersian(item.quantity)} بسته</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-mono font-bold font-vazir text-white">{toPersian((item.product.priceValue * item.quantity).toLocaleString())} تومان</span>
|
<span className="text-xs font-mono font-bold font-vazir">{toPersian((item.product.priceValue * item.quantity).toLocaleString())}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 pt-8 border-t border-white/20">
|
<div className="space-y-4 pt-8 border-t border-white/10">
|
||||||
<div className="flex justify-between text-sm font-bold text-white/80">
|
<div className="flex justify-between text-sm font-bold text-white/60">
|
||||||
<span>مجموع اقلام</span>
|
<span>مجموع اقلام</span>
|
||||||
<span className="font-vazir text-white">{toPersian(getSubtotal().toLocaleString())} تومان</span>
|
<span className="font-vazir">{toPersian(getSubtotal().toLocaleString())} تومان</span>
|
||||||
</div>
|
</div>
|
||||||
{isSubscribed && (
|
{isSubscribed && (
|
||||||
<div className="flex justify-between text-sm font-bold text-green-400">
|
<div className="flex justify-between text-sm font-bold text-green-400">
|
||||||
<span>تخفیف اشتراک (۵٪-)</span>
|
<span>تخفیف اشتراک (۵٪-)</span>
|
||||||
<span className="font-vazir text-green-400">{toPersian(getDiscount().toLocaleString())} تومان</span>
|
<span className="font-vazir">{toPersian(getDiscount().toLocaleString())} تومان</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{charityDonation > 0 && (
|
{charityDonation > 0 && (
|
||||||
<div className="flex justify-between text-sm font-bold text-pink-400">
|
<div className="flex justify-between text-sm font-bold text-pink-400">
|
||||||
<span>ردپای مهربانی</span>
|
<span>ردپای مهربانی</span>
|
||||||
<span className="font-vazir text-pink-400">{toPersian(charityDonation.toLocaleString())} تومان+</span>
|
<span className="font-vazir">{toPersian(charityDonation.toLocaleString())} تومان+</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-between text-sm font-bold text-white/80">
|
<div className="flex justify-between text-sm font-bold text-white/60">
|
||||||
<span>هزینه ارسال</span>
|
<span>هزینه ارسال</span>
|
||||||
<span className="text-green-400 uppercase tracking-widest text-[10px] font-black">رایگان</span>
|
<span className="text-green-400 uppercase tracking-widest text-[10px] font-black">رایگان</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-end pt-4">
|
<div className="flex justify-between items-end pt-4">
|
||||||
<span className="text-xl font-black italic text-white">مبلغ نهایی</span>
|
<span className="text-xl font-black italic">مبلغ نهایی</span>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<p className="text-3xl font-black text-canina-gold leading-none font-vazir">{toPersian(getTotal().toLocaleString())}</p>
|
<p className="text-3xl font-black text-canina-blue leading-none font-vazir">{toPersian(getTotal().toLocaleString())}</p>
|
||||||
<p className="text-[10px] text-white/50 font-bold mt-1">تومان</p>
|
<p className="text-[10px] text-white/30 font-bold mt-1">تومان</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -437,14 +317,12 @@ export default function CheckoutPage() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="chk-coupon"
|
name="coupon"
|
||||||
name="coupon"
|
type="text"
|
||||||
type="text"
|
placeholder="کد تخفیف (مثلاً: CANINO2024)"
|
||||||
autoComplete="off"
|
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs focus:ring-1 focus:ring-canina-blue outline-none"
|
||||||
placeholder="کد تخفیف (مثلاً: CANINO2024)"
|
/>
|
||||||
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs focus:ring-1 focus:ring-canina-blue outline-none"
|
|
||||||
/>
|
|
||||||
<button type="submit" className="bg-white/10 px-4 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-white/20 transition-all">اعمال</button>
|
<button type="submit" className="bg-white/10 px-4 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-white/20 transition-all">اعمال</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -300,39 +300,39 @@ export default function Header({
|
|||||||
|
|
||||||
<div className="flex gap-1.5 sm:gap-2 items-center h-12">
|
<div className="flex gap-1.5 sm:gap-2 items-center h-12">
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
<div className="flex items-center h-full gap-1 sm:gap-2 p-1 bg-medical-gray-50/50 border border-medical-gray-100 rounded-3xl">
|
<div className="flex items-center h-full gap-1.5 sm:gap-2 p-1 bg-medical-gray-50/50 border border-medical-gray-100 rounded-3xl">
|
||||||
{/* User Icon/Name (Direct link to dashboard) */}
|
{/* User Name (Direct link to dashboard) */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-1 sm:gap-2.5 px-2 sm:px-4 py-2 hover:bg-white rounded-2xl cursor-pointer transition-all border border-transparent hover:border-medical-gray-200 min-w-0"
|
className="flex items-center gap-1.5 sm:gap-2.5 px-2.5 sm:px-4 py-2 hover:bg-white rounded-2xl cursor-pointer transition-all border border-transparent hover:border-medical-gray-200"
|
||||||
onClick={() => onNavigate("user-dashboard")}
|
onClick={() => onNavigate("user-dashboard")}
|
||||||
>
|
>
|
||||||
<User className="w-4 h-4 sm:w-5 sm:h-5 text-canina-blue flex-shrink-0" />
|
<User className="w-4 h-4 sm:w-5 sm:h-5 text-canina-blue" />
|
||||||
<span className="hidden sm:inline text-[14px] font-black font-vazir text-medical-gray-900 leading-none truncate max-w-[80px]">
|
<span className="text-[12px] sm:text-[14px] font-black font-vazir text-medical-gray-900 leading-none">
|
||||||
{profile.firstName}
|
{profile.firstName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Separator Line */}
|
{/* Separator Line */}
|
||||||
<div className="w-[1.5px] h-4 sm:h-5 bg-medical-gray-200 flex-shrink-0" />
|
<div className="w-[1.5px] h-4 sm:h-5 bg-medical-gray-200" />
|
||||||
|
|
||||||
{/* Pet Icon & Switcher */}
|
{/* Pet Name & Switcher */}
|
||||||
<div className="relative h-full flex items-center" ref={petMenuRef}>
|
<div className="relative h-full flex items-center" ref={petMenuRef}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1 sm:gap-2.5 px-2 sm:px-4 py-2 rounded-2xl transition-all cursor-pointer min-w-0",
|
"flex items-center gap-1.5 sm:gap-2.5 px-2.5 sm:px-4 py-2 rounded-2xl transition-all cursor-pointer",
|
||||||
isPetSwitcherOpen ? "bg-canina-blue text-white shadow-lg shadow-canina-blue/20" : "text-canina-blue hover:bg-white"
|
isPetSwitcherOpen ? "bg-canina-blue text-white shadow-lg shadow-canina-blue/20" : "text-canina-blue hover:bg-white"
|
||||||
)}
|
)}
|
||||||
onClick={() => setIsPetSwitcherOpen(!isPetSwitcherOpen)}
|
onClick={() => setIsPetSwitcherOpen(!isPetSwitcherOpen)}
|
||||||
>
|
>
|
||||||
{activePet?.type === "گربه" ? (
|
{activePet?.type === "گربه" ? (
|
||||||
<Cat className={cn("w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0", isPetSwitcherOpen ? "text-white" : "text-canina-blue/60")} />
|
<Cat className={cn("w-4 h-4 sm:w-5 sm:h-5", isPetSwitcherOpen ? "text-white" : "text-canina-blue/60")} />
|
||||||
) : (
|
) : (
|
||||||
<Dog className={cn("w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0", isPetSwitcherOpen ? "text-white" : "text-canina-blue/60")} />
|
<Dog className={cn("w-4 h-4 sm:w-5 sm:h-5", isPetSwitcherOpen ? "text-white" : "text-canina-blue/60")} />
|
||||||
)}
|
)}
|
||||||
<span className={cn("hidden sm:inline text-[14px] font-black font-vazir leading-none truncate max-w-[80px]", isPetSwitcherOpen ? "text-white" : "text-canina-blue")}>
|
<span className={cn("text-[12px] sm:text-[14px] font-black font-vazir leading-none", isPetSwitcherOpen ? "text-white" : "text-canina-blue")}>
|
||||||
{activePet?.name || "بدون پت"}
|
{activePet?.name || "بدون پت"}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className={cn("w-3 h-3 sm:w-4 sm:h-4 transition-transform flex-shrink-0", isPetSwitcherOpen ? "rotate-180" : "")} />
|
<ChevronDown className={cn("w-3.5 h-3.5 sm:w-4 sm:h-4 transition-transform", isPetSwitcherOpen ? "rotate-180" : "")} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user