feat: dynamic UI texts system + admin panel editor + Vazirmatn local font + bug fixes
- Add getText() system for all homepage sections (Header, Hero, FeaturedProducts, SmartAdvisor, VetGallery, Footer)
- Add UITexts admin page in frontend/admin-panel with search, group collapse, per-key save
- Add seed-ui-texts.ts with 102 texts in 9 groups
- Add local VazirmatnVariable.woff2 font, remove CDN + Inter references
- Fix hero image path, productService return type { data, meta }
- Fix ArchivePage pagination + CATEGORIES list + breadcrumbs
- Fix BlogPage/Wiki page links + brightness-0 invert removal
- Fix IngredientWiki search + product links + placeholder
- Fix all getProducts() callers for { data } destructuring
- Fix sitemap.ts, CartDrawer, B2BPortal, CheckoutPage type issues
- Add .gitignore for /fonts/ and /backend/uploads/
This commit is contained in:
parent
ab7ed279c8
commit
1a0c98a34e
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@ coverage/
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
/fonts/
|
||||
/backend/uploads/
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "Canina Ballaststoff Mix",
|
||||
"category_slug": "immune",
|
||||
"description": "فیبر میکس کانینا حاوی منابع فیبر گیاهی است که به طور روزانه رژیم غذایی خام پت را تقویت می کند. این فیبرها به عنوان پری بیوتیک برای فلور پایدار روده مهم هستند. حرکت طبیعی روده را تحریک کرده، مدفوع را حجیم می کند و تاثیر مثبتی بر تخلیه غدد مقعدی دارد. سیستم ایمنی روده را تقویت کرده و انرژی باکتری های روده بزرگ را تامین می کند. بدون غلات و پروتئین حیوانی بوده و برای آلرژی غذایی مناسب است.",
|
||||
"ingredients": "تفاله سیب، پودر یونجه سبز، فیبر نخود فرنگی، تفاله هویج، کیک بذر کتان، کمپلکس الیگوساکارید و بتاگلوکان، مخمر آبجو، کنگر فرنگی، خاک دیاتومه، روغن کلزا، کربنات کلسیم. مواد تشکیل دهنده: پروتئین خام 11.9%، چربی خام 6.3%، فیبر خام 27.3%.",
|
||||
"usage_and_dosage": "مناسب برای توله سگ ها، بچه گربه ها و حیوانات مسن، باردار و شیرده. پودر را با کمی آب گرم مخلوط کنید. مقدار مصرف: 1 گرم به ازای هر کیلوگرم وزن بدن، حداکثر 15 گرم در روز.",
|
||||
@ -10,6 +11,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina CANHYDROX GAG",
|
||||
"category_slug": "joints",
|
||||
"description": "ویژه سگ های بزرگ و سنگین وزن برای درمان بافت همبند، مفاصل، استخوان ها، تاندون ها و غضروف. ایده آل برای سگ های در مرحله رشد، مسن و سگ های با عملکرد بالا. از تشکیل کالوس و استحکام استخوان پشتیبانی کرده و ظرفیت بافر مفاصل را بهبود می بخشد.",
|
||||
"ingredients": "30% تری کلسیم فسفات، هیدروکسی آپاتیت، مخمر آبجو، 10% پودر صدف سبز، جلبک دریایی. ویتامین سی، ویتامین ایی، سلنیوم و متیونین.",
|
||||
"usage_and_dosage": "سگ های جوان از 8 هفتگی: برای پیشگیری 2 قرص و برای اندام های بدشکل 4 قرص به ازای هر 10 کیلوگرم تا 15 ماهگی. سگ های مسن و عملکردی: 4 قرص به ازای هر 10 کیلوگرم. پس از شکستگی: 4 قرص برای دوره نقاهت.",
|
||||
@ -19,6 +21,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Eierschalenpulver",
|
||||
"category_slug": "energy",
|
||||
"description": "پودر پوسته تخم مرغ برای تامین کلسیم، سلامت دندان ها و فعالیت عضلات. جایگزین پودر استخوان برای سگ های دارای آلرژی به پروتئین گوشت. مناسب برای تکمیل وعده های غذایی گوشتی خام (B.A.R.F).",
|
||||
"ingredients": "100% پوسته تخم مرغ آسیاب شده. خاکستر خام 96%، کلسیم 36%، فسفر 0.1%.",
|
||||
"usage_and_dosage": "0.2 گرم به ازای هر کیلوگرم وزن بدن، حداکثر 16 گرم برای هر حیوان در روز.",
|
||||
@ -28,6 +31,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina FLEXAN",
|
||||
"category_slug": "joints",
|
||||
"description": "پشتیبانی از سیستم اسکلتی عضلانی، مفاصل متحرک و تناسب اندام. پپتیدهای کلاژن غضروف را بازسازی کرده و پودر صدف لب سبز بافت همبند را تقویت می کند.",
|
||||
"ingredients": "90% کلاژن، 10% پودر گوشت صدف لب سبز. پروتئین خام 94.2%.",
|
||||
"usage_and_dosage": "تغذیه روزانه برای توله سگ ها تا مسن. 2 گرم به ازای هر 10 کیلوگرم وزن بدن، حداکثر 15 گرم در روز.",
|
||||
@ -37,6 +41,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina HERZ VITAL",
|
||||
"category_slug": "energy",
|
||||
"description": "برای سگ ها و گربه های مسن دارای نارسایی قلبی. ال کارنیتین بر تامین انرژی قلب و عضلات تاثیر گذاشته و نشاط و رفاه حیوان را افزایش می دهد.",
|
||||
"ingredients": "دکستروز، مخمر غیر فعال، زالزالک. حاوی ال کارنیتین.",
|
||||
"usage_and_dosage": "نصف قاشق چایخوری به ازای هر 10 کیلوگرم وزن بدن، حداکثر 3 قاشق چایخوری. به مدت 6 ماه همراه غذا تجویز شود.",
|
||||
@ -46,6 +51,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina IMMUN-BOOSTER Paste",
|
||||
"category_slug": "immune",
|
||||
"description": "بازیابی و نقاهت تغذیه ای با چگالی انرژی بالا. ایمونوگلوبولین های آغوز سیستم دفاعی را تقویت کرده و برای نقاهت پس از زایمان، تمرین و استرس عالی است. انتروکوک فاسیوم فلور روده را سالم نگه می دارد.",
|
||||
"ingredients": "روغن های گیاهی، 20% پودر آغوز، شکر انگور. ویتامین آ، د3، ایی و تثبیت کننده فلور روده.",
|
||||
"usage_and_dosage": "توله ها 0.25 میلی لیتر، سگ های متوسط 0.5-1 میلی لیتر، سگ های بزرگ 1-4 میلی لیتر در روز تا زمان بهبودی کامل.",
|
||||
@ -55,6 +61,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Katzenmilch",
|
||||
"category_slug": "nutrition",
|
||||
"description": "غذای کامل جایگزین شیر مادر با ویتامین ها و مواد معدنی. مناسب بچه گربه های بی مادر از 1 روزگی، گربه های مسن، بیمار و شیرده.",
|
||||
"ingredients": "لاکتوز، پودر آب پنیر، چربی نارگیل، روغن ماهی، پروتئین 30%. غنی از ویتامین ها و 100 میلی گرم تورین.",
|
||||
"usage_and_dosage": "1 قسمت شیر با 3 قسمت آب ولرم مخلوط شود. به بچه گربه های 1 تا 14 روزه هر 2 ساعت تغذیه شود.",
|
||||
@ -64,6 +71,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Lachsöl",
|
||||
"category_slug": "special-care",
|
||||
"description": "روغن ماهی سالمون 100% خالص سرشار از اسیدهای چرب امگا 3 (EPA و DHA). متابولیسم را تنظیم کرده و از بازسازی پوست و رشد مو پشتیبانی می کند.",
|
||||
"ingredients": "100% روغن ماهی سالمون. اسیدهای چرب چند غیراشباع 56%، امگا 3 (14%) و امگا 6 (11%).",
|
||||
"usage_and_dosage": "2 میلی لیتر به ازای هر 10 کیلوگرم وزن بدن، حداکثر 10 میلی لیتر در روز مخلوط با خوراک.",
|
||||
@ -73,6 +81,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Marine-Ölmischung PREMIUM",
|
||||
"category_slug": "special-care",
|
||||
"description": "ترکیبی هماهنگ از روغن های خوراکی که متابولیسم ارگانیسم را ارتقا می دهد. از کمبود مواد مغذی در رژیم های خام جلوگیری می کند. روغن آرگان و گل رز به عنوان آنتی اکسیدان و بازسازی کننده عمل می کنند.",
|
||||
"ingredients": "روغن ماهی، روغن گردو، روغن آرگان، روغن دانه گل رز. چربی خام 100%.",
|
||||
"usage_and_dosage": "قابل افزودن به تمام وعده های غذایی برای بهینه سازی ویتامین ها و اسیدهای چرب امگا.",
|
||||
@ -82,6 +91,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Moortränke",
|
||||
"category_slug": "immune",
|
||||
"description": "عصاره پیت حاوی مواد هومیک برای محافظت از غشاهای مخاطی و سیستم ایمنی روده. ایده آل برای نفخ، اسهال، پس از درمان آنتی بیوتیک، تحریک اشتها و خون سازی.",
|
||||
"ingredients": "100% پیت، پیت مایع (بدون افزودنی و نگهدارنده).",
|
||||
"usage_and_dosage": "قبل از استفاده تکان دهید. سگ های کوچک 2.5 تا 5 میلی لیتر، سگ های متوسط 10 میلی لیتر، سگ های بزرگتر 15 تا 20 میلی لیتر. از 20 میلی لیتر تجاوز نکند.",
|
||||
@ -91,6 +101,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina PETVITAL Arthro-Tabletten",
|
||||
"category_slug": "joints",
|
||||
"description": "متابولیسم استخوان را بهبود بخشیده و مفاصل ضعیف را تقویت می کند. عصاره بید و زنجبیل به عنوان آنتی اکسیدان عمل کرده و خاصیت ارتجاعی استخوان را افزایش می دهند.",
|
||||
"ingredients": "گیاهان خشک (بید و زنجبیل)، ویتامین ایی، ویتامین ب1، آهن، مس، روی.",
|
||||
"usage_and_dosage": "توله سگ ها: 2 قرص به ازای 10 کیلوگرم برای 1 ماه. سگ های بالغ: 5 قرص به ازای 10 کیلوگرم برای 3 ماه (سالی یکبار تکرار شود).",
|
||||
@ -100,6 +111,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina PETVITAL Bio-Aktivator",
|
||||
"category_slug": "energy",
|
||||
"description": "حاوی سرم خون گاو برای تحریک اشتها و تسریع بهبودی در دوره نقاهت. رفاه و هماهنگی متابولیسم را بهبود می بخشد.",
|
||||
"ingredients": "سرم خون گاو، اتانول 164.4 گرم (به عنوان طعم دهنده).",
|
||||
"usage_and_dosage": "1 قطره به ازای هر کیلوگرم وزن بدن، حداکثر 20 قطره در روز. قطره ها به مدت 4 هفته داده شوند، سپس 2 هفته استراحت.",
|
||||
@ -109,6 +121,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina PETVITAL Biotin-Tabs",
|
||||
"category_slug": "special-care",
|
||||
"description": "دوز بالای بیوتین (ویتامین H) برای جلوگیری از ریزش مو، ایجاد پوشش براق، رنگدانه های قوی و پنجه های محکم.",
|
||||
"ingredients": "دکستروز، آب پنیر، نشاسته، سلولز. بیوتین 1000000 میکروگرم، ویتامین ب2، اسید نیکوتینیک.",
|
||||
"usage_and_dosage": "یک قرص به ازای هر 10 کیلوگرم وزن بدن، حداکثر 5 قرص در روز.",
|
||||
@ -118,6 +131,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina PETVITAL ENERGY-GEL",
|
||||
"category_slug": "energy",
|
||||
"description": "کنسانتره انرژی برای اصلاح سریع کمبودهای مواد مغذی. ایده آل برای دوره نقاهت جهت تحریک اشتها، دوره های رشد، بارداری و شیردهی.",
|
||||
"ingredients": "روغن ها، چربی ها، ویتامین ایی، ویتامین ب1، ب2، ب6، ب12.",
|
||||
"usage_and_dosage": "1 قاشق چایخوری (3 میلی لیتر) به ازای 10 کیلوگرم وزن بدن، حداکثر 4 قاشق چایخوری در روز.",
|
||||
@ -127,6 +141,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina PETVITAL MINERAL-Tabs",
|
||||
"category_slug": "energy",
|
||||
"description": "انواع مختلف کلسیم برای برطرف کردن کمبود و تشکیل بافت استخوانی. عالی برای دوره رشد، بارداری و شیردهی جهت تامین نیاز کلسیم.",
|
||||
"ingredients": "کربنات کلسیم، مخمر آبجو، ویتامین د3 100000 (برای ذخیره کلسیم در استخوان).",
|
||||
"usage_and_dosage": "سگ های بالغ: 2 قرص به ازای 10 کیلوگرم. باردار و شیرده: 3 قرص به ازای 10 کیلوگرم. مصرف ویتامین D2 همزمان مجاز نیست.",
|
||||
@ -136,6 +151,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina PETVITAL GAG",
|
||||
"category_slug": "joints",
|
||||
"description": "تامین گلیکوزآمینوگلیکان ها، تاندون ها، رباط ها و غضروف مفاصل را تثبیت می کند. شامل پودر صدف لب سبز و اسیدهای آمینه برای عملکرد بهینه سیستم حرکتی.",
|
||||
"ingredients": "پودر ماهی، پودر جلبک دریایی، پودر گوشت صدف لب سبز. دی ال متیونین، ویتامین ایی، سلنیوم.",
|
||||
"usage_and_dosage": "پودر: 1 قاشق چایخوری به ازای 10 کیلوگرم (حداکثر 4 قاشق). قرص: 1 قرص به ازای 10 کیلوگرم (حداکثر 5 قرص).",
|
||||
@ -147,6 +163,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina PETVITAL VITAMIN-Tabs",
|
||||
"category_slug": "energy",
|
||||
"description": "اصلاح کمبود ویتامین، جلوگیری از خستگی، عدم تناسب و اختلالات رشد. ایده آل برای مادران باردار و شرایط استرس زا.",
|
||||
"ingredients": "مخمر آبجو، سلولز، ویتامین های آ، ب1 تا ب12، د3، ایی، کا3.",
|
||||
"usage_and_dosage": "1 قرص به ازای هر 10 کیلوگرم وزن بدن در روز.",
|
||||
@ -156,6 +173,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Rinderblut Pulver",
|
||||
"category_slug": "energy",
|
||||
"description": "پودر خون گاو، منبع ارزشمند آهن و هموگلوبین برای انتقال اکسیژن. مکمل عالی برای رژیم گوشت خام (B.A.R.F) جهت تامین خون از دست رفته و رشد عضلات. مناسب دوره نقاهت و سگ های ورزشی.",
|
||||
"ingredients": "خون کامل گاو خشک شده. پروتئین خام 92%، آهن.",
|
||||
"usage_and_dosage": "0.3 گرم به ازای هر کیلوگرم وزن بدن (حداکثر 12 گرم در روز) مخلوط با خوراک خام برای 6 ماه.",
|
||||
@ -166,6 +184,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Rinderfett Pulver",
|
||||
"category_slug": "energy",
|
||||
"description": "منبع انرژی طبیعی از چربی گوشت گاو آب پز بدون شکر. عالی برای حیوانات لاغر، سگ های بسیار فعال، و جبران کمبود چربی در وعده های خام.",
|
||||
"ingredients": "72% چربی گوشت گاو، 27% آب گوشت گاو خشک شده. انرژی: 740 کیلوکالری.",
|
||||
"usage_and_dosage": "نصف قاشق چایخوری به ازای هر 10 کیلوگرم وزن بدن، حداکثر 3 قاشق چایخوری در روز همراه غذای مرطوب.",
|
||||
@ -175,6 +194,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Schwarz Kümmel Samen",
|
||||
"category_slug": "general",
|
||||
"description": "دانه های زیره سیاه سرشار از اسیدهای چرب غیر اشباع که هضم و متابولیسم را تنظیم می کنند. سیستم دفاعی و عملکرد سگ را تقویت می کند.",
|
||||
"ingredients": "100% دانه های زیره سیاه مصری.",
|
||||
"usage_and_dosage": "1 قاشق چایخوری به ازای 10 کیلوگرم وزن بدن، حداکثر 4 قاشق چایخوری مخلوط با آب ولرم. در بارداری استفاده نشود.",
|
||||
@ -184,6 +204,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Seealgen / Bio-Seealgenmehl",
|
||||
"category_slug": "special-care",
|
||||
"description": "جلبک دریایی ارگانیک سرشار از مواد معدنی و اسیدهای آمینه که با ید و روی، رنگدانه های پوست، بینی و پوشش را پشتیبانی می کند. هضم و سیستم ایمنی را بهبود می بخشد.",
|
||||
"ingredients": "جلبک دریایی حاوی لیزین، سلولز و ویتامین سی.",
|
||||
"usage_and_dosage": "قرص: 2 عدد به ازای 10 کیلوگرم. پودر: نصف قاشق چایخوری به ازای 10 کیلوگرم وزن بدن. جهت تامین نیاز ید.",
|
||||
@ -195,6 +216,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina TAURIN Für Katzen",
|
||||
"category_slug": "special-care",
|
||||
"description": "اسید آمینه ضروری برای گربه ها که سیستم ایمنی، بینایی و قلب را پشتیبانی می کند. کمبود آن باعث نابینایی و اختلالات عصبی می شود.",
|
||||
"ingredients": "مخمر آبجو، تورین 700 گرم.",
|
||||
"usage_and_dosage": "گربه تا 6 کیلو: نصف قاشق چایخوری. بالای 6 کیلو و باردار/شیرده: 1 قاشق چایخوری در روز.",
|
||||
@ -204,6 +226,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Velox Gelenkenergie",
|
||||
"category_slug": "joints",
|
||||
"description": "کنسانتره صدف لب سبز نیوزیلندی برای بازسازی مفاصل، رباط ها و غضروف. افزایش استقامت مفاصل توله ها در حال رشد و سگ های پیر.",
|
||||
"ingredients": "100% پودر گوشت صدف لب سبز.",
|
||||
"usage_and_dosage": "1 قاشق چایخوری به ازای 10 کیلوگرم وزن بدن (حداکثر 2 قاشق) در روز به مدت 4 هفته یا مداوم.",
|
||||
@ -213,6 +236,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Welpenbrei",
|
||||
"category_slug": "nutrition",
|
||||
"description": "فرنی با پروتئین و ویتامین بالا که پرورش سالم توله سگ را از هفته 3 تضمین می کند.",
|
||||
"ingredients": "غلات، گوشت، فرآورده های لبنی، ماهی، پروتئین 27%.",
|
||||
"usage_and_dosage": "1 قسمت فرنی را با 3 قسمت آب گرم (50 درجه سانتیگراد) مخلوط کنید. مصرف ویتامین D2 همزمان مجاز نیست.",
|
||||
@ -222,6 +246,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Welpenmilch",
|
||||
"category_slug": "nutrition",
|
||||
"description": "شیر کامل جایگزین شیر مادر با انرژی و ویتامین بالا برای توله سگ های بی مادر از روز 1.",
|
||||
"ingredients": "لاکتوز، پودر آب پنیر، روغن ماهی، پروتئین 30%، تورین، ویتامین ها.",
|
||||
"usage_and_dosage": "1 قسمت شیر با 4 قسمت آب 50 درجه مخلوط شود. تغذیه هر 2 ساعت در ابتدا و سپس هر 4 ساعت.",
|
||||
@ -231,6 +256,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina PETVITAL Bio-Insect Shocker",
|
||||
"category_slug": "special-care",
|
||||
"description": "اسپری دفع کننده محیطی کک، شپش و کنه. تخم ها و انگل های خارجی را به طور موثر از بین می برد.",
|
||||
"ingredients": "15 گرم در لیتر دودکانوئیک اسید، 5 گرم در لیتر عصاره مارگوسا.",
|
||||
"usage_and_dosage": "هر 4 تا 6 هفته به صورت اسپری از فاصله 30 سانتی متری روی محل زندگی و خواب حیوان استفاده شود.",
|
||||
@ -241,6 +267,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina Mikrosilber Zahngel",
|
||||
"category_slug": "special-care",
|
||||
"description": "ژل دندانی با میکروسیلور که پلاک، جرم و التهاب لثه را هدف قرار می دهد. بیوفلاونوئیدها از باکتری های پلاک جلوگیری می کنند.",
|
||||
"ingredients": "آب، نقره، گلیسیرین، عصاره دانه گریپ فروت.",
|
||||
"usage_and_dosage": "روزانه عصرها 1 ساعت پس از غذا روی دندان های آسیاب مالیده شود. سگ تا 10 کیلو: 1 سانتی متر ژل.",
|
||||
@ -250,6 +277,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina NovaGard Green Augenpflege",
|
||||
"category_slug": "special-care",
|
||||
"description": "قطره چشم برای تمیز کردن و مراقبت از چشم های گریان و گرفتگی چشم. نقره کلوئیدی از التهاب جلوگیری می کند.",
|
||||
"ingredients": "آب بدون مواد معدنی، هیالورونات سدیم، نقره کلوئیدی.",
|
||||
"usage_and_dosage": "چند قطره روی کیسه ملتحمه ریخته و اطراف چشم پاک شود.",
|
||||
@ -259,6 +287,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Canina NovaGard Green Pfotenpflege",
|
||||
"category_slug": "special-care",
|
||||
"description": "کرم مراقبت از پنجه 100% طبیعی برای تسکین پوست خارش دار، التیام زخم ها و خراشیدگی پوست سگ ها در زمستان و تابستان.",
|
||||
"ingredients": "وازلین، روغن پالپ سنجد دریایی، روغن کالاندولا، پانتنول.",
|
||||
"usage_and_dosage": "چندین بار در روز روی نواحی آسیب دیده بمالید و ماساژ دهید.",
|
||||
|
||||
150
backend/prisma/seed-ui-texts.ts
Normal file
150
backend/prisma/seed-ui-texts.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const UI_TEXTS: Record<string, string> = {
|
||||
// === Header / Navigation ===
|
||||
"shipping_notice": "ارسال رایگان برای سفارشهای بالای ۱۵۰ هزار تومان",
|
||||
"brand_name_fa": "ایران",
|
||||
"brand_subtitle": "نماینده رسمی Canina Pharma GmbH آلمان",
|
||||
"nav_solutions": "راهکارهای درمانی",
|
||||
"nav_products": "محصولات تخصصی",
|
||||
"nav_wiki": "دانشنامه علمی",
|
||||
"nav_blog": "مجله سلامت پت",
|
||||
"nav_pet_profiles": "شناسنامه پتها",
|
||||
"nav_search_placeholder": "جستجوی محصولات، مقالات سلامت و...",
|
||||
"nav_search_button": "جستجو",
|
||||
"nav_pet_switcher_title": "تغییر پت یا مدیریت حساب",
|
||||
"nav_register_pet": "ثبت همدم (Pet) جدید",
|
||||
"nav_wallet_orders": "کیف پول و سفارشات",
|
||||
"nav_logout": "خروج از حساب کانینا",
|
||||
"nav_login": "ورود / ثبتنام",
|
||||
"nav_cart": "سبد خرید",
|
||||
|
||||
// === Hero Section ===
|
||||
"hero_badge": "تخصص دارویی از آلمان",
|
||||
"hero_title": "تخصص آلمانی در خدمت\nسلامت پتهای خانگی",
|
||||
"hero_desc": "بیش از <span className=\"font-bold text-canina-blue\">۴۰ سال</span> تجربه نوآورانه در تولید مکملهای درمانی با بالاترین استاندارد کیفی «گرید دارویی اختصاصی». راهکار هوشمند برای هر نیاز بالینی.",
|
||||
"hero_btn_advisor": "دستیار سلامت پت",
|
||||
"hero_btn_products": "مشاهده محصولات",
|
||||
"hero_image_badge": "سرآمد علمی در پزشکی پتها",
|
||||
"hero_image_title": "مکملهای تایید شده دامپزشکی",
|
||||
"hero_quality_standard": "استاندارد کیفی آلمان",
|
||||
"hero_stat_founded": "سال تأسیس ۱۹۸۴",
|
||||
"hero_stat_agencies": "نمایندگی فعال",
|
||||
"hero_stat_german_formula": "فرمول آلمانی",
|
||||
|
||||
// === Smart Advisor ===
|
||||
"advisor_badge": "تکنولوژی پایش هوشمند",
|
||||
"advisor_title": "دستیار سلامت کانینا",
|
||||
"advisor_subtitle": "فقط چند قدم تا صدور شناسنامه هوشمند و دریافت پیشنهادات تخصصی",
|
||||
"advisor_step1_title": "گام اول: هویت بصری",
|
||||
"advisor_step1_desc": "همدم شما رو با چه اسمی صدا میزنید؟",
|
||||
"advisor_pet_name_label": "نام پت",
|
||||
"advisor_pet_name_placeholder": "لوسی، تدی...",
|
||||
"advisor_pet_breed_label": "نژاد",
|
||||
"advisor_pet_breed_placeholder": "ژرمن، پرشین...",
|
||||
"advisor_submit_btn": "ثبت هویت و ادامه",
|
||||
"advisor_validation_warning": "⚠️ لطفاً ابتدا نام و نژاد پت را در بالا وارد کنید.",
|
||||
"advisor_step2_title": "گام دوم: پایش فیزیکی",
|
||||
"advisor_step2_desc": "اطلاعات فیزیکی دقیق به دوزبندی صحیح مکملها کمک میکند",
|
||||
"advisor_age_label": "سن حیوان (سال)",
|
||||
"advisor_weight_label": "وزن حیوان (کیلوگرم)",
|
||||
"advisor_activity_label": "سطح فعالیت روزانه",
|
||||
"advisor_back": "قبلی",
|
||||
"advisor_next_final": "گام نهایی سلامت",
|
||||
"advisor_step3_title": "گام آخر: سوابق و حساسیتها",
|
||||
"advisor_step3_desc": "در صورت وجود هر یک از موارد زیر، آن را تیک بزنید",
|
||||
"advisor_complete": "تکمیل و صدور شناسنامه",
|
||||
|
||||
// === Featured Products ===
|
||||
"featured_badge": "بیشترین انتخاب توسط دامپزشکان",
|
||||
"featured_title_line1": "محصولات برگزیده و راهکارهای",
|
||||
"featured_title_line2": "درمان تخصصی",
|
||||
"featured_desc": "محصولات دارویی کانینا با استفاده از دانش پیشرفته بیوتکنولوژی و مواد اولیه ارگانیک نایاب فرآوری شدهاند.",
|
||||
"featured_view_all": "مشاهده تمامی محصولات",
|
||||
"product_view_details": "مشاهده جزییات",
|
||||
"featured_no_products_title": "هیچ محصولی یافت نشد",
|
||||
"featured_no_products_desc": "در حال حاضر هیچ محصول برجستهای برای نمایش وجود ندارد. لطفاً در پنل ادمین محصولات برگزیده را تنظیم کنید.",
|
||||
|
||||
// === Vet Gallery / Videos ===
|
||||
"videos_badge": "Vet Insights",
|
||||
"videos_title": "مشاوره ویدئویی",
|
||||
"videos_title_highlight": "دوست دامپزشک پت شما",
|
||||
"videos_desc": "متخصصین آلمانی کانینا مستقیماً با شما صحبت میکنند. نحوه مصرف، مزایای رقابتی و دانش تخصصی پشت هر محصول.",
|
||||
"videos_view_all": "مشاهده همه ویدئوها",
|
||||
|
||||
// === Navigation Cards (Home Page) ===
|
||||
"nav_card_shop_title": "فروشگاه تخصصی",
|
||||
"nav_card_shop_desc": "دسترسی به تمامی محصولات بر اساس علائم بالینی و نیازهای پت شما.",
|
||||
"nav_card_shop_cta": "کاتالوگ کامل محصولات",
|
||||
"nav_card_wiki_title": "دانشنامه علمی",
|
||||
"nav_card_wiki_desc": "آشنایی با ترکیبات نایاب و استانداردهای دارویی محصولات آلمانی کانینا.",
|
||||
"nav_card_wiki_cta": "مطالعه مقالات تخصصی",
|
||||
|
||||
// === About Section ===
|
||||
"about_label": "میراث ما از آلمان",
|
||||
"about_title": "چرا برند آلمانی Canina مرجع دامپزشکان است؟",
|
||||
"about_item1_title": "مواد اولیه نایاب",
|
||||
"about_item1_desc": "استفاده از پودر صدف لبسبز اصل نیوزیلند و مواد ارگانیک با گرید دارویی.",
|
||||
"about_item2_title": "فاقد مواد نگهدارنده",
|
||||
"about_item2_desc": "تمامی محصولات ۱۰۰٪ طبیعی و فاقد رنگهای مصنوعی و طعمدهندههای شیمیایی هستند.",
|
||||
"about_item3_title": "تاییدیه اروپا",
|
||||
"about_item3_desc": "مطابق با سختگیرانهترین استانداردهای ایمنی مواد غذایی و دارویی در اتحادیه اروپا.",
|
||||
|
||||
// === CTA Section ===
|
||||
"cta_title": "میخواهید بدانید کدام محصول برای پت شما مناسبتر است؟",
|
||||
"cta_desc": "تیم متخصص دامپزشکی کانینا ایران آماده پاسخگویی به سوالات شماست.",
|
||||
"cta_btn_primary": "دریافت رژیم مکمل رایگان",
|
||||
"cta_btn_secondary": "پروانه دامپزشکی و اصالت کالا",
|
||||
|
||||
// === Footer ===
|
||||
"footer_brand_title": "کانینا ایران",
|
||||
"footer_brand_subtitle": "نماینده رسمی در ایران",
|
||||
"footer_brand_desc": "تدارک هوشمندانه سلامت برای همراهان وفادار شما. واردکننده انحصاری مکملهای درمانی با گرید دارویی اختصاصی از آلمان با سابقه ۴۰ سال نوآوری.",
|
||||
"footer_links_title": "دسترسی سریع",
|
||||
"footer_link_products": "محصولات تخصصی ۲۰۲۴",
|
||||
"footer_link_blog": "مجله سلامت پت (وبلاگ)",
|
||||
"footer_link_profiles": "شناسنامه پتها",
|
||||
"footer_link_wiki": "دانشنامه ترکیبات",
|
||||
"footer_link_b2b": "پنل سفارش عمده (B2B)",
|
||||
"footer_contact_title": "اطلاعات تماس نمایندگی",
|
||||
"footer_phone_label": "خط ویژه فروش",
|
||||
"footer_phone": "۰۲۱-۸۸۸۸ ۴۴۴۴",
|
||||
"footer_email_label": "مکاتبات رسمی",
|
||||
"footer_email": "info@canino-iran.com",
|
||||
"footer_address_label": "دفتر مرکزی",
|
||||
"footer_address": "تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹",
|
||||
"footer_auth_title": "اصالت کالا تضمین میشود",
|
||||
"footer_auth_desc": "تمامی محصولات دارای لیبل اصالت وزارت بهداشت و QR Code اختصاصی هستند.",
|
||||
"footer_auth_badge": "اصالت آلمانی کانینا",
|
||||
"footer_copyright": "© ۲۰۲۶ کانینا ایران. تمامی حقوق محفوظ است.",
|
||||
"footer_about": "درباره ما",
|
||||
"footer_contact": "تماس با ما",
|
||||
"footer_privacy": "حریم خصوصی",
|
||||
};
|
||||
|
||||
async function main() {
|
||||
console.log('Seeding UI texts...');
|
||||
|
||||
const entries = Object.entries(UI_TEXTS);
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
await prisma.uiText.upsert({
|
||||
where: { key },
|
||||
update: { value },
|
||||
create: { key, value },
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Seeded ${entries.length} UI texts.`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@ -14,6 +14,7 @@ import Categories from './pages/Categories';
|
||||
import Blogs from './pages/Blogs';
|
||||
import Wiki from './pages/Wiki';
|
||||
import Pets from './pages/Pets';
|
||||
import UITexts from './pages/UITexts';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@ -33,7 +34,8 @@ function App() {
|
||||
<Route path="categories" element={<Categories />} />
|
||||
<Route path="blogs" element={<Blogs />} />
|
||||
<Route path="wiki" element={<Wiki />} />
|
||||
<Route path="pets" element={<Pets />} />
|
||||
<Route path="pets" element={<Pets />} />
|
||||
<Route path="ui-texts" element={<UITexts />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Home, Users, ShoppingBag, ShoppingCart, Tag, Settings, LogOut, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard } from 'lucide-react';
|
||||
import { Home, Users, ShoppingBag, ShoppingCart, Tag, Settings, LogOut, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages } from 'lucide-react';
|
||||
|
||||
const menuGroups = [
|
||||
{
|
||||
@ -36,6 +36,7 @@ const menuGroups = [
|
||||
title: 'سیستم',
|
||||
items: [
|
||||
{ icon: Settings, label: 'تنظیمات', path: '/settings' },
|
||||
{ icon: Languages, label: 'متون رابط کاربری', path: '/ui-texts' },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
220
frontend/admin-panel/src/pages/UITexts.tsx
Normal file
220
frontend/admin-panel/src/pages/UITexts.tsx
Normal file
@ -0,0 +1,220 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Languages, Save, Search, CheckCircle, Loader2 } from 'lucide-react';
|
||||
import api from '../services/api';
|
||||
import Spinner from '../components/ui/Spinner';
|
||||
|
||||
const GROUPS: Record<string, string[]> = {
|
||||
"هدر و ناوبری": [
|
||||
"shipping_notice", "brand_name_fa", "brand_subtitle",
|
||||
"nav_solutions", "nav_products", "nav_wiki", "nav_blog", "nav_pet_profiles",
|
||||
"nav_search_placeholder", "nav_search_button", "nav_pet_switcher_title",
|
||||
"nav_register_pet", "nav_wallet_orders", "nav_logout", "nav_login", "nav_cart",
|
||||
],
|
||||
"بنر اصلی (Hero)": [
|
||||
"hero_badge", "hero_title", "hero_desc",
|
||||
"hero_btn_advisor", "hero_btn_products",
|
||||
"hero_image_badge", "hero_image_title", "hero_quality_standard",
|
||||
"hero_stat_founded", "hero_stat_agencies", "hero_stat_german_formula",
|
||||
],
|
||||
"دستیار سلامت": [
|
||||
"advisor_badge", "advisor_title", "advisor_subtitle",
|
||||
"advisor_step1_title", "advisor_step1_desc",
|
||||
"advisor_pet_name_label", "advisor_pet_name_placeholder",
|
||||
"advisor_pet_breed_label", "advisor_pet_breed_placeholder",
|
||||
"advisor_submit_btn", "advisor_validation_warning",
|
||||
"advisor_step2_title", "advisor_step2_desc",
|
||||
"advisor_age_label", "advisor_weight_label", "advisor_activity_label",
|
||||
"advisor_back", "advisor_next_final",
|
||||
"advisor_step3_title", "advisor_step3_desc", "advisor_complete",
|
||||
],
|
||||
"محصولات برگزیده": [
|
||||
"featured_badge", "featured_title_line1", "featured_title_line2",
|
||||
"featured_desc", "featured_view_all", "product_view_details",
|
||||
"featured_no_products_title", "featured_no_products_desc",
|
||||
],
|
||||
"ویدئوها": [
|
||||
"videos_badge", "videos_title", "videos_title_highlight",
|
||||
"videos_desc", "videos_view_all",
|
||||
],
|
||||
"کارتهای ناوبری": [
|
||||
"nav_card_shop_title", "nav_card_shop_desc", "nav_card_shop_cta",
|
||||
"nav_card_wiki_title", "nav_card_wiki_desc", "nav_card_wiki_cta",
|
||||
],
|
||||
"درباره ما": [
|
||||
"about_label", "about_title",
|
||||
"about_item1_title", "about_item1_desc",
|
||||
"about_item2_title", "about_item2_desc",
|
||||
"about_item3_title", "about_item3_desc",
|
||||
],
|
||||
"CTA": [
|
||||
"cta_title", "cta_desc", "cta_btn_primary", "cta_btn_secondary",
|
||||
],
|
||||
"فوتر": [
|
||||
"footer_brand_title", "footer_brand_subtitle", "footer_brand_desc",
|
||||
"footer_links_title", "footer_link_products", "footer_link_blog",
|
||||
"footer_link_profiles", "footer_link_wiki", "footer_link_b2b",
|
||||
"footer_contact_title", "footer_phone_label", "footer_phone",
|
||||
"footer_email_label", "footer_email", "footer_address_label", "footer_address",
|
||||
"footer_auth_title", "footer_auth_desc", "footer_auth_badge",
|
||||
"footer_copyright", "footer_about", "footer_contact", "footer_privacy",
|
||||
],
|
||||
};
|
||||
|
||||
export default function UITexts() {
|
||||
const [texts, setTexts] = useState<Record<string, string>>({});
|
||||
const [edits, setEdits] = useState<Record<string, string>>({});
|
||||
const [search, setSearch] = useState("");
|
||||
const [saving, setSaving] = useState<string | null>(null);
|
||||
const [savedKey, setSavedKey] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(() => new Set(Object.keys(GROUPS)));
|
||||
|
||||
useEffect(() => {
|
||||
api.get('/settings/ui-texts').then(res => {
|
||||
const obj: Record<string, string> = {};
|
||||
if (Array.isArray(res.data)) {
|
||||
res.data.forEach((item: { key: string; value: string }) => {
|
||||
obj[item.key] = item.value;
|
||||
});
|
||||
}
|
||||
setTexts(obj);
|
||||
setLoading(false);
|
||||
}).catch(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleChange = (key: string, value: string) => {
|
||||
setEdits(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleSave = async (key: string) => {
|
||||
const value = edits[key] ?? texts[key];
|
||||
setSaving(key);
|
||||
try {
|
||||
await api.patch(`/settings/ui-texts/${key}`, { value });
|
||||
setTexts(prev => ({ ...prev, [key]: value }));
|
||||
setEdits(prev => { const n = { ...prev }; delete n[key]; return n; });
|
||||
setSavedKey(key);
|
||||
setTimeout(() => setSavedKey(null), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to save:', err);
|
||||
} finally {
|
||||
setSaving(null);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleGroup = (group: string) => {
|
||||
setExpandedGroups(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(group)) next.delete(group);
|
||||
else next.add(group);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const filteredGroups = Object.entries(GROUPS)
|
||||
.map(([group, keys]) => ({
|
||||
group,
|
||||
keys: keys.filter(k =>
|
||||
!search || k.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(texts[k] || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(edits[k] || "").toLowerCase().includes(search.toLowerCase())
|
||||
),
|
||||
}))
|
||||
.filter(g => g.keys.length > 0);
|
||||
|
||||
const unsavedCount = Object.keys(edits).length;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-black text-gray-900 flex items-center gap-2">
|
||||
<Languages className="w-6 h-6 text-purple-600" />
|
||||
متون رابط کاربری
|
||||
</h2>
|
||||
<p className="text-gray-500 font-medium mt-1">ویرایش تمامی متون سایت از هدر تا فوتر</p>
|
||||
</div>
|
||||
{unsavedCount > 0 && (
|
||||
<span className="bg-amber-100 text-amber-700 px-3 py-1 rounded-full text-xs font-bold">
|
||||
{unsavedCount} تغییر ذخیره نشده
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="جستجو در کلیدها یا متنها..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
className="w-full bg-white border border-gray-200 rounded-xl py-3 pr-11 pl-4 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center p-12"><Spinner size="lg" className="text-purple-600" /></div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredGroups.map(({ group, keys }) => {
|
||||
const isExpanded = expandedGroups.has(group);
|
||||
return (
|
||||
<div key={group} className="bg-white rounded-2xl border border-gray-200 overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggleGroup(group)}
|
||||
className="w-full bg-gray-50 px-6 py-4 border-b border-gray-200 flex items-center justify-between hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<h3 className="font-black text-gray-800">{group}</h3>
|
||||
<span className="text-xs text-gray-400 font-bold">{keys.length} متن</span>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="divide-y divide-gray-100">
|
||||
{keys.map(key => {
|
||||
const current = edits[key] !== undefined ? edits[key] : texts[key] || "";
|
||||
const isDirty = edits[key] !== undefined;
|
||||
const isSaved = savedKey === key;
|
||||
const isSavingThis = saving === key;
|
||||
|
||||
return (
|
||||
<div key={key} className="px-6 py-4 flex items-start gap-4">
|
||||
<div className="w-52 shrink-0 pt-2">
|
||||
<code className="text-[10px] text-gray-500 bg-gray-100 px-2 py-1 rounded font-mono break-all">
|
||||
{key}
|
||||
</code>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<textarea
|
||||
value={current}
|
||||
onChange={e => handleChange(key, e.target.value)}
|
||||
rows={current.length > 80 ? 3 : 1}
|
||||
className={`w-full border rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500/20 resize-y transition-colors ${
|
||||
isDirty ? 'border-amber-300 bg-amber-50' : 'border-gray-200 bg-white'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleSave(key)}
|
||||
disabled={!isDirty || isSavingThis}
|
||||
className={`mt-1 p-2 rounded-lg transition-all ${
|
||||
isSaved
|
||||
? 'bg-green-100 text-green-600'
|
||||
: isDirty
|
||||
? 'bg-purple-600 text-white hover:bg-purple-700 shadow-sm'
|
||||
: 'bg-gray-100 text-gray-300 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{isSavingThis ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,10 +1,16 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100;300;400;500;700;900&family=Inter:wght@400;500;600;700;800;900&display=swap');
|
||||
@font-face {
|
||||
font-family: 'Vazirmatn';
|
||||
src: url('/fonts/VazirmatnVariable.woff2') format('woff2 supports variations'),
|
||||
url('/fonts/VazirmatnVariable.woff2') format('woff2-variations');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Vazirmatn", "Inter", sans-serif;
|
||||
--font-sans: "Vazirmatn", sans-serif;
|
||||
--font-vazir: "Vazirmatn", sans-serif;
|
||||
--font-inter: "Inter", sans-serif;
|
||||
|
||||
/* Brand Colors */
|
||||
--color-canina-blue: #1E4053;
|
||||
@ -27,11 +33,11 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
font-family: "Vazirmatn", "Inter", sans-serif !important;
|
||||
font-family: "Vazirmatn", sans-serif !important;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
font-family: "Vazirmatn", "Inter", sans-serif !important;
|
||||
font-family: "Vazirmatn", sans-serif !important;
|
||||
@apply bg-medical-gray-50 text-medical-gray-900 antialiased;
|
||||
font-feature-settings: "ss01", "ss02", "cv01", "cv11";
|
||||
}
|
||||
|
||||
@ -40,9 +40,9 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
|
||||
// Dynamic products
|
||||
try {
|
||||
const products = await productService.getProducts();
|
||||
const { data: products } = await productService.getProducts();
|
||||
const productRoutes: MetadataRoute.Sitemap = products.map((product) => ({
|
||||
url: `${baseUrl}/shop/${product.id}`,
|
||||
url: `${baseUrl}/shop/${product.slug || product.id}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.7,
|
||||
|
||||
@ -28,10 +28,12 @@ import SafeImage from "./SafeImage";
|
||||
|
||||
const CATEGORIES = [
|
||||
{ id: "all", label: "همه محصولات", icon: <Activity className="w-4 h-4" /> },
|
||||
{ id: "joints", label: "حرکتی و مفاصل", icon: <HeartPulse className="w-4 h-4" /> },
|
||||
{ id: "immune", label: "تقویت و ایمنی", icon: <ShieldCheck className="w-4 h-4" /> },
|
||||
{ id: "joints", label: "مفاصل و استخوان", icon: <HeartPulse className="w-4 h-4" /> },
|
||||
{ id: "immune", label: "ایمنی و گوارش", icon: <ShieldCheck className="w-4 h-4" /> },
|
||||
{ id: "energy", label: "ویتامین و انرژی", icon: <Activity className="w-4 h-4" /> },
|
||||
{ id: "special-care", label: "پلتفرم مراقبت ویژه", icon: <Stethoscope className="w-4 h-4" /> },
|
||||
{ id: "special-care", label: "مراقبت ویژه", icon: <Stethoscope className="w-4 h-4" /> },
|
||||
{ id: "general", label: "تقویت عمومی", icon: <Heart className="w-4 h-4" /> },
|
||||
{ id: "nutrition", label: "تغذیه تخصصی", icon: <Sparkles className="w-4 h-4" /> },
|
||||
];
|
||||
|
||||
const CATEGORY_MAP: Record<string, { category?: string; query?: string; symptoms?: string[] }> = {
|
||||
@ -178,6 +180,18 @@ export default function ArchivePage({
|
||||
const [searchQuery, setSearchQuery] = useState(initialSearch);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [meta, setMeta] = useState({ total: 0, lastPage: 1 });
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
all: "همه محصولات",
|
||||
joints: "مفاصل و استخوان",
|
||||
immune: "ایمنی و گوارش",
|
||||
energy: "ویتامین و انرژی",
|
||||
"special-care": "مراقبت ویژه",
|
||||
general: "تقویت عمومی",
|
||||
nutrition: "تغذیه تخصصی",
|
||||
};
|
||||
|
||||
// Debug Log for Search Params
|
||||
useEffect(() => {
|
||||
@ -228,18 +242,22 @@ export default function ArchivePage({
|
||||
const fetchProducts = async () => {
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const data = await productService.getProducts({
|
||||
const res = await productService.getProducts({
|
||||
category: selectedCategory,
|
||||
petType: selectedPet,
|
||||
query: searchQuery
|
||||
query: searchQuery,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
});
|
||||
|
||||
let result = data;
|
||||
let result = res.data;
|
||||
if (activeSymptoms.length > 0) {
|
||||
result = result.filter(p => p.symptoms.some(s => activeSymptoms.includes(s)));
|
||||
}
|
||||
|
||||
setFilteredProducts(result);
|
||||
setMeta(res.meta);
|
||||
setPage(1);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch products:", error);
|
||||
toast.error("خطا در دریافت لیست محصولات از سرور. لطفاً صفحه را رفرش کنید.");
|
||||
@ -251,6 +269,28 @@ export default function ArchivePage({
|
||||
fetchProducts();
|
||||
}, [selectedCategory, selectedPet, searchQuery, activeSymptoms]);
|
||||
|
||||
const loadMore = async () => {
|
||||
if (page >= meta.lastPage) return;
|
||||
try {
|
||||
const res = await productService.getProducts({
|
||||
category: selectedCategory,
|
||||
petType: selectedPet,
|
||||
query: searchQuery,
|
||||
page: page + 1,
|
||||
limit: 20,
|
||||
});
|
||||
let newItems = res.data;
|
||||
if (activeSymptoms.length > 0) {
|
||||
newItems = newItems.filter(p => p.symptoms.some(s => activeSymptoms.includes(s)));
|
||||
}
|
||||
setFilteredProducts(prev => [...prev, ...newItems]);
|
||||
setMeta(res.meta);
|
||||
setPage(prev => prev + 1);
|
||||
} catch (error) {
|
||||
console.error("Failed to load more:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSymptom = (s: string) => {
|
||||
setActiveSymptoms(prev => prev.includes(s) ? prev.filter(item => item !== s) : [...prev, s]);
|
||||
};
|
||||
@ -277,11 +317,17 @@ export default function ArchivePage({
|
||||
</div>
|
||||
|
||||
{/* Desktop Breadcrumbs */}
|
||||
<div className="hidden lg:flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-10">
|
||||
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-10">
|
||||
<span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/')}>خانه</span>
|
||||
<ChevronLeft className="w-2.5 h-2.5" />
|
||||
<span className="text-canina-blue">کاتالوگ کامل محصولات</span>
|
||||
</div>
|
||||
<span className="cursor-pointer hover:text-canina-blue" onClick={() => { setSelectedCategory("all"); }}>کاتالوگ</span>
|
||||
{selectedCategory !== "all" && (
|
||||
<>
|
||||
<ChevronLeft className="w-2.5 h-2.5" />
|
||||
<span className="text-canina-blue">{CATEGORY_LABELS[selectedCategory] || selectedCategory}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-10">
|
||||
|
||||
@ -383,6 +429,7 @@ export default function ArchivePage({
|
||||
))}
|
||||
</div>
|
||||
) : filteredProducts.length > 0 ? (
|
||||
<>
|
||||
<div key="grid" className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{filteredProducts.map((p: Product) => (
|
||||
@ -390,6 +437,17 @@ export default function ArchivePage({
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
{page < meta.lastPage && (
|
||||
<div className="flex justify-center mt-10">
|
||||
<button
|
||||
onClick={loadMore}
|
||||
className="bg-canina-blue text-white px-8 py-3 rounded-xl font-black text-sm hover:scale-105 transition-transform shadow-lg shadow-canina-blue/20"
|
||||
>
|
||||
بارگذاری محصولات بیشتر
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div key="empty" className="bg-white rounded-[3rem] p-20 text-center border-2 border-dashed border-medical-gray-200 flex flex-col items-center">
|
||||
<div className="w-20 h-20 bg-medical-gray-50 rounded-full flex items-center justify-center text-medical-gray-300 mb-6">
|
||||
|
||||
@ -29,7 +29,7 @@ export default function B2BPortal({ onClose }: { onClose: () => void }) {
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
productService.getProducts().then(data => setProducts(data));
|
||||
productService.getProducts({ limit: 999 }).then(res => setProducts(res.data));
|
||||
}, []);
|
||||
|
||||
const filteredProducts = products.filter(p =>
|
||||
|
||||
@ -24,7 +24,7 @@ export default function BlogPage({ blogs = [] }: { blogs: BlogPost[] }) {
|
||||
const [products, setProducts] = React.useState<Product[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
productService.getProducts().then(data => setProducts(data));
|
||||
productService.getProducts({ limit: 10 }).then(res => setProducts(res.data));
|
||||
}, []);
|
||||
|
||||
if (selectedPost) {
|
||||
@ -100,7 +100,7 @@ export default function BlogPage({ blogs = [] }: { blogs: BlogPost[] }) {
|
||||
{products.slice(4, 7).map(p => (
|
||||
<div
|
||||
key={p.id}
|
||||
onClick={() => router.push(`/shop/${p.id}`)}
|
||||
onClick={() => router.push(`/shop/${p.slug || p.id}`)}
|
||||
className="bg-white/10 border border-white/20 p-4 rounded-3xl flex items-center gap-4 cursor-pointer hover:bg-white/20 transition-all group"
|
||||
>
|
||||
<div className="w-16 h-16 bg-white rounded-2xl p-2 shrink-0 group-hover:scale-110 transition-transform">
|
||||
@ -305,10 +305,10 @@ export default function BlogPage({ blogs = [] }: { blogs: BlogPost[] }) {
|
||||
{products.slice(0, 2).map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
onClick={() => router.push(`/shop/${p.id}`)}
|
||||
className="bg-white/10 border border-white/20 p-6 rounded-3xl cursor-pointer hover:bg-white/20 transition-all text-center group"
|
||||
>
|
||||
<img src={p.image} alt={p.name} className="w-20 h-20 mx-auto mb-4 object-contain filter brightness-0 invert group-hover:scale-110 transition-transform" />
|
||||
onClick={() => router.push(`/shop/${p.slug || p.id}`)}
|
||||
className="bg-white/10 border border-white/20 p-6 rounded-3xl cursor-pointer hover:bg-white/20 transition-all text-center group"
|
||||
>
|
||||
<img src={p.image} alt={p.name} className="w-20 h-20 mx-auto mb-4 object-contain group-hover:scale-110 transition-transform" />
|
||||
<h5 className="text-[10px] font-black">{p.name}</h5>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -38,7 +38,7 @@ export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasLachsOl && hasCanhydrox) {
|
||||
productService.getProducts().then(prods => {
|
||||
productService.getProducts({ limit: 999 }).then(({ data: prods }) => {
|
||||
const found = prods.find(p => p.id === 'lachs-ol' || p.artNo === 'lachs-ol');
|
||||
setSuggestedProduct(found || null);
|
||||
});
|
||||
|
||||
@ -37,7 +37,7 @@ export default function CheckoutPage() {
|
||||
const [paymentMethod, setPaymentMethod] = useState<string>('online');
|
||||
|
||||
useEffect(() => {
|
||||
productService.getProducts().then(data => setDbProducts(data));
|
||||
productService.getProducts({ limit: 999 }).then(res => setDbProducts(res.data));
|
||||
}, []);
|
||||
|
||||
const subtotal = getSubtotal();
|
||||
|
||||
@ -8,8 +8,10 @@ import { Eye, ShoppingCart, ShieldCheck, Heart, AlertCircle, Sparkles } from "lu
|
||||
import SafeImage from "./SafeImage";
|
||||
import { ProductCardSkeleton } from "./Skeleton";
|
||||
import Link from 'next/link';
|
||||
import { useSettingsStore } from "../lib/store/settingsStore";
|
||||
|
||||
function ProductCard({ product, onClick }: { product: Product; key?: string | number; onClick: (p: Product) => void }) {
|
||||
const getText = useSettingsStore(state => state.getText);
|
||||
const { getActivePet } = usePetStore();
|
||||
const activePet = getActivePet();
|
||||
|
||||
@ -86,7 +88,7 @@ function ProductCard({ product, onClick }: { product: Product; key?: string | nu
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xl font-black text-medical-gray-900 font-vazir">{product.price}</div>
|
||||
<button className="text-xs font-black text-canina-blue uppercase tracking-widest border-b-2 border-canina-blue pb-1 hover:text-medical-gray-900 hover:border-medical-gray-900 transition-all">
|
||||
مشاهده جزییات
|
||||
{getText('product_view_details', "مشاهده جزییات")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -100,6 +102,7 @@ import { toast } from 'sonner';
|
||||
|
||||
export default function FeaturedProducts() {
|
||||
const router = useRouter();
|
||||
const getText = useSettingsStore(state => state.getText);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
@ -123,21 +126,21 @@ export default function FeaturedProducts() {
|
||||
<div className="max-w-7xl mx-auto px-4">
|
||||
<div className="flex flex-col lg:flex-row items-end justify-between mb-16 gap-6">
|
||||
<div className="lg:w-2/3">
|
||||
<div className="text-canina-blue text-xs font-bold uppercase tracking-[0.2em] mb-4 font-vazir">بیشترین انتخاب توسط دامپزشکان</div>
|
||||
<div className="text-canina-blue text-xs font-bold uppercase tracking-[0.2em] mb-4 font-vazir">{getText('featured_badge', "بیشترین انتخاب توسط دامپزشکان")}</div>
|
||||
<h2 className="text-3xl lg:text-5xl font-black text-medical-gray-900 leading-[1.2]">
|
||||
محصولات برگزیده و راهکارهای <br />
|
||||
<span className="italic text-canina-blue">درمان تخصصی</span>
|
||||
{getText('featured_title_line1', "محصولات برگزیده و راهکارهای")} <br />
|
||||
<span className="italic text-canina-blue">{getText('featured_title_line2', "درمان تخصصی")}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="lg:w-1/3 text-left lg:text-right">
|
||||
<p className="text-medical-gray-500 text-sm leading-relaxed mb-4">
|
||||
محصولات دارویی کانینا با استفاده از دانش پیشرفته بیوتکنولوژی و مواد اولیه ارگانیک نایاب فرآوری شدهاند.
|
||||
{getText('featured_desc', "محصولات دارویی کانینا با استفاده از دانش پیشرفته بیوتکنولوژی و مواد اولیه ارگانیک نایاب فرآوری شدهاند.")}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push('/shop')}
|
||||
className="inline-flex items-center gap-2 text-canina-blue font-bold text-sm hover:translate-x-1 transition-transform rtl:hover:-translate-x-1"
|
||||
>
|
||||
مشاهده تمامی محصولات
|
||||
{getText('featured_view_all', "مشاهده تمامی محصولات")}
|
||||
<span className="text-lg">←</span>
|
||||
</button>
|
||||
</div>
|
||||
@ -153,9 +156,9 @@ export default function FeaturedProducts() {
|
||||
) : (
|
||||
<div className="col-span-full py-16 px-8 flex flex-col items-center justify-center text-center bg-white rounded-[2.5rem] border border-medical-gray-200 shadow-xl max-w-lg mx-auto">
|
||||
<AlertCircle className="w-12 h-12 text-canina-teal mb-4" />
|
||||
<h3 className="text-xl font-black text-medical-gray-900 mb-2 font-vazir">هیچ محصولی یافت نشد</h3>
|
||||
<h3 className="text-xl font-black text-medical-gray-900 mb-2 font-vazir">{getText('featured_no_products_title', "هیچ محصولی یافت نشد")}</h3>
|
||||
<p className="text-sm text-medical-gray-500 font-medium font-vazir leading-relaxed">
|
||||
در حال حاضر هیچ محصول برجستهای برای نمایش وجود ندارد. لطفاً در پنل ادمین محصولات برگزیده را تنظیم کنید.
|
||||
{getText('featured_no_products_desc', "در حال حاضر هیچ محصول برجستهای برای نمایش وجود ندارد. لطفاً در پنل ادمین محصولات برگزیده را تنظیم کنید.")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { Phone, Mail, MapPin, Instagram, ShieldCheck, Globe } from "lucide-react";
|
||||
import { useSettingsStore } from "../lib/store/settingsStore";
|
||||
|
||||
export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
|
||||
onNavigate: (v: any) => void,
|
||||
onShopNavigate: (c?: string) => void,
|
||||
onB2BOpen?: () => void
|
||||
}) {
|
||||
const getText = useSettingsStore(state => state.getText);
|
||||
return (
|
||||
<footer className="bg-medical-gray-900 text-white pt-24 pb-12 font-vazir" dir="rtl">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
@ -19,12 +21,12 @@ export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
|
||||
<img src="https://canina.de/media/fb/d5/47/1683116813/Logo.png" alt="Canina Logo" className="w-full h-auto" referrerPolicy="no-referrer" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-black italic tracking-tighter">کانینا ایران</h3>
|
||||
<p className="text-[10px] text-white/40 font-bold uppercase tracking-widest">نماینده رسمی در ایران</p>
|
||||
<h3 className="text-2xl font-black italic tracking-tighter">{getText('footer_brand_title', "کانینا ایران")}</h3>
|
||||
<p className="text-[10px] text-white/40 font-bold uppercase tracking-widest">{getText('footer_brand_subtitle', "نماینده رسمی در ایران")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-white/60 leading-relaxed font-medium">
|
||||
تدارک هوشمندانه سلامت برای همراهان وفادار شما. واردکننده انحصاری مکملهای درمانی با گرید دارویی اختصاصی از آلمان با سابقه ۴۰ سال نوآوری.
|
||||
{getText('footer_brand_desc', "تدارک هوشمندانه سلامت برای همراهان وفادار شما. واردکننده انحصاری مکملهای درمانی با گرید دارویی اختصاصی از آلمان با سابقه ۴۰ سال نوآوری.")}
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
{[Instagram, Globe].map((Icon, i) => (
|
||||
@ -37,34 +39,34 @@ export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
|
||||
|
||||
{/* Quick Links */}
|
||||
<div className="space-y-8">
|
||||
<h4 className="text-lg font-black text-canina-blue italic underline decoration-white/20 underline-offset-8">دسترسی سریع</h4>
|
||||
<h4 className="text-lg font-black text-canina-blue italic underline decoration-white/20 underline-offset-8">{getText('footer_links_title', "دسترسی سریع")}</h4>
|
||||
<ul className="space-y-4 text-sm font-bold text-white/60">
|
||||
<li onClick={() => onShopNavigate()} className="hover:text-white hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-canina-blue group-hover:w-4 transition-all" />
|
||||
محصولات تخصصی ۲۰۲۴
|
||||
{getText('footer_link_products', "محصولات تخصصی ۲۰۲۴")}
|
||||
</li>
|
||||
<li onClick={() => onNavigate("blog")} className="hover:text-white hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-canina-blue group-hover:w-4 transition-all" />
|
||||
مجله سلامت پت (وبلاگ)
|
||||
{getText('footer_link_blog', "مجله سلامت پت (وبلاگ)")}
|
||||
</li>
|
||||
<li onClick={() => onNavigate("profile")} className="hover:text-white hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-canina-blue group-hover:w-4 transition-all" />
|
||||
شناسنامه پتها
|
||||
{getText('footer_link_profiles', "شناسنامه پتها")}
|
||||
</li>
|
||||
<li onClick={() => onNavigate("wiki")} className="hover:text-white hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-canina-blue group-hover:w-4 transition-all" />
|
||||
دانشنامه ترکیبات
|
||||
{getText('footer_link_wiki', "دانشنامه ترکیبات")}
|
||||
</li>
|
||||
<li onClick={onB2BOpen} className="hover:text-canina-blue hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group text-blue-300">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-canina-blue group-hover:w-4 transition-all" />
|
||||
پنل سفارش عمده (B2B)
|
||||
{getText('footer_link_b2b', "پنل سفارش عمده (B2B)")}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Location & Contact */}
|
||||
<div className="space-y-8 lg:col-span-2">
|
||||
<h4 className="text-lg font-black text-canina-blue italic underline decoration-white/20 underline-offset-8">اطلاعات تماس نمایندگی</h4>
|
||||
<h4 className="text-lg font-black text-canina-blue italic underline decoration-white/20 underline-offset-8">{getText('footer_contact_title', "اطلاعات تماس نمایندگی")}</h4>
|
||||
<div className="grid md:grid-cols-2 gap-8">
|
||||
<div className="space-y-6">
|
||||
<div className="flex gap-4 group">
|
||||
@ -72,8 +74,8 @@ export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
|
||||
<Phone className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] text-white/40 font-black uppercase mb-1">خط ویژه فروش</p>
|
||||
<p className="text-lg font-black tracking-widest text-left" dir="ltr">۰۲۱-۸۸۸۸ ۴۴۴۴</p>
|
||||
<p className="text-[10px] text-white/40 font-black uppercase mb-1">{getText('footer_phone_label', "خط ویژه فروش")}</p>
|
||||
<p className="text-lg font-black tracking-widest text-left" dir="ltr">{getText('footer_phone', "۰۲۱-۸۸۸۸ ۴۴۴۴")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4 group">
|
||||
@ -81,8 +83,8 @@ export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
|
||||
<Mail className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] text-white/40 font-black uppercase mb-1">مکاتبات رسمی</p>
|
||||
<p className="text-sm font-bold">info@canino-iran.com</p>
|
||||
<p className="text-[10px] text-white/40 font-black uppercase mb-1">{getText('footer_email_label', "مکاتبات رسمی")}</p>
|
||||
<p className="text-sm font-bold">{getText('footer_email', "info@canino-iran.com")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -92,8 +94,8 @@ export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
|
||||
<MapPin className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] text-white/40 font-black uppercase mb-1">دفتر مرکزی</p>
|
||||
<p className="text-sm font-bold leading-relaxed">تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹</p>
|
||||
<p className="text-[10px] text-white/40 font-black uppercase mb-1">{getText('footer_address_label', "دفتر مرکزی")}</p>
|
||||
<p className="text-sm font-bold leading-relaxed">{getText('footer_address', "تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -104,22 +106,22 @@ export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
|
||||
<ShieldCheck className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="text-sm font-black">اصالت کالا تضمین میشود</h5>
|
||||
<p className="text-[10px] text-white/40 font-bold">تمامی محصولات دارای لیبل اصالت وزارت بهداشت و QR Code اختصاصی هستند.</p>
|
||||
<h5 className="text-sm font-black">{getText('footer_auth_title', "اصالت کالا تضمین میشود")}</h5>
|
||||
<p className="text-[10px] text-white/40 font-bold">{getText('footer_auth_desc', "تمامی محصولات دارای لیبل اصالت وزارت بهداشت و QR Code اختصاصی هستند.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs font-black italic text-white/20">اصالت آلمانی کانینا</div>
|
||||
<div className="text-xs font-black italic text-white/20">{getText('footer_auth_badge', "اصالت آلمانی کانینا")}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="pt-12 border-t border-white/5 flex flex-col md:flex-row items-center justify-between gap-6 text-[10px] font-black text-white/20 uppercase tracking-[0.3em]">
|
||||
<div>© ۲۰۲۶ کانینا ایران. تمامی حقوق محفوظ است.</div>
|
||||
<div>{getText('footer_copyright', "© ۲۰۲۶ کانینا ایران. تمامی حقوق محفوظ است.")}</div>
|
||||
<div className="flex gap-8 font-vazir">
|
||||
<button onClick={() => onNavigate("home")} className="hover:text-white transition-colors">درباره ما</button>
|
||||
<button onClick={() => onNavigate("home")} className="hover:text-white transition-colors">تماس با ما</button>
|
||||
<button className="hover:text-white transition-colors">حریم خصوصی</button>
|
||||
<button onClick={() => onNavigate("home")} className="hover:text-white transition-colors">{getText('footer_about', "درباره ما")}</button>
|
||||
<button onClick={() => onNavigate("home")} className="hover:text-white transition-colors">{getText('footer_contact', "تماس با ما")}</button>
|
||||
<button className="hover:text-white transition-colors">{getText('footer_privacy', "حریم خصوصی")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -5,6 +5,7 @@ import { motion, AnimatePresence } from "motion/react";
|
||||
import { useCartStore } from "../lib/store/cartStore";
|
||||
import { usePetStore } from "../lib/store/usePetStore";
|
||||
import { useUserStore } from "../lib/store/userStore";
|
||||
import { useSettingsStore } from "../lib/store/settingsStore";
|
||||
import { toast } from "sonner";
|
||||
import HeaderButton from "./HeaderButton";
|
||||
import AuthModal from "./AuthModal";
|
||||
@ -66,6 +67,7 @@ export default function Header({
|
||||
const { pets, activePetId, setActivePet, getActivePet } = usePetStore();
|
||||
const { role, isLoggedIn, logout, profile } = useUserStore();
|
||||
const activePet = getActivePet();
|
||||
const getText = useSettingsStore(state => state.getText);
|
||||
|
||||
// Click Outside logic
|
||||
useEffect(() => {
|
||||
@ -92,7 +94,7 @@ export default function Header({
|
||||
|
||||
<div className="bg-canina-gold text-canina-dark text-xs font-black py-2.5 text-center font-vazir tracking-wider flex items-center justify-center gap-2">
|
||||
<span>🚚</span>
|
||||
<span>ارسال رایگان برای سفارشهای بالای ۱۵۰ هزار تومان</span>
|
||||
<span>{getText('shipping_notice', "ارسال رایگان برای سفارشهای بالای ۱۵۰ هزار تومان")}</span>
|
||||
</div>
|
||||
|
||||
<header className="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-medical-gray-100 shadow-sm">
|
||||
@ -117,8 +119,8 @@ export default function Header({
|
||||
>
|
||||
<div className="w-12 h-12 bg-canina-blue rounded-2xl flex items-center justify-center text-white font-bold text-2xl group-hover:bg-medical-gray-900 transition-all shadow-xl shadow-canina-blue/20 italic">C</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-canina-blue font-black text-2xl tracking-tighter leading-none italic font-inter">Canina <span className="text-sm not-italic font-medium border-l-2 border-medical-gray-100 pl-2 ml-2 font-vazir">ایران</span></span>
|
||||
<span className="text-[10px] text-medical-gray-400 font-bold uppercase tracking-widest leading-none mt-1.5 font-vazir">نماینده رسمی Canina Pharma GmbH آلمان</span>
|
||||
<span className="text-canina-blue font-black text-2xl tracking-tighter leading-none italic font-sans">Canina <span className="text-sm not-italic font-medium border-l-2 border-medical-gray-100 pl-2 ml-2 font-vazir">{getText('brand_name_fa', "ایران")}</span></span>
|
||||
<span className="text-[10px] text-medical-gray-400 font-bold uppercase tracking-widest leading-none mt-1.5 font-vazir">{getText('brand_subtitle', "نماینده رسمی Canina Pharma GmbH آلمان")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -131,7 +133,7 @@ export default function Header({
|
||||
onMouseLeave={() => setIsMegaMenuOpen(false)}
|
||||
>
|
||||
<button className={`flex items-center justify-center gap-1.5 px-4 py-3 rounded-xl text-sm font-black transition-all whitespace-nowrap focus-visible:ring-4 focus-visible:ring-canina-blue/30 focus-visible:outline-none min-h-[48px] ${isMegaMenuOpen ? 'bg-medical-gray-50 text-canina-blue' : 'text-medical-gray-600 hover:bg-medical-gray-50 hover:text-canina-blue'} font-vazir`}>
|
||||
راهکارهای درمانی
|
||||
{getText('nav_solutions', "راهکارهای درمانی")}
|
||||
<ChevronDown className={`w-4 h-4 transition-transform duration-300 ${isMegaMenuOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
@ -181,10 +183,10 @@ export default function Header({
|
||||
</div>
|
||||
|
||||
{[
|
||||
{ id: 'shop', label: 'محصولات تخصصی', action: () => onShopNavigate() },
|
||||
{ id: 'wiki', label: 'دانشنامه علمی', action: () => onNavigate('wiki') },
|
||||
{ id: 'blog', label: 'مجله سلامت پت', action: () => onNavigate('blog') },
|
||||
{ id: 'profile', label: 'شناسنامه پتها', action: () => onNavigate({ view: 'profile', subview: 'index' }) }
|
||||
{ id: 'shop', label: getText('nav_products', 'محصولات تخصصی'), action: () => onShopNavigate() },
|
||||
{ id: 'wiki', label: getText('nav_wiki', 'دانشنامه علمی'), action: () => onNavigate('wiki') },
|
||||
{ id: 'blog', label: getText('nav_blog', 'مجله سلامت پت'), action: () => onNavigate('blog') },
|
||||
{ id: 'profile', label: getText('nav_pet_profiles', 'شناسنامه پتها'), action: () => onNavigate({ view: 'profile', subview: 'index' }) }
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
@ -217,11 +219,11 @@ export default function Header({
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
onBlur={() => !searchQuery && setIsSearchFocused(false)}
|
||||
placeholder="جستجوی محصولات، مقالات سلامت و..."
|
||||
placeholder={getText('nav_search_placeholder', "جستجوی محصولات، مقالات سلامت و...")}
|
||||
className="w-full bg-transparent px-4 py-2 text-[12px] font-bold outline-none font-vazir text-medical-gray-900"
|
||||
/>
|
||||
<button type="submit" className="px-4 py-2 bg-canina-blue text-white text-[10px] font-black hover:bg-medical-gray-900 transition-colors">
|
||||
جستجو
|
||||
{getText('nav_search_button', "جستجو")}
|
||||
</button>
|
||||
</form>
|
||||
</motion.div>
|
||||
@ -282,7 +284,7 @@ export default function Header({
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
className="absolute top-[125%] left-0 w-72 bg-white border border-medical-gray-100 shadow-2xl rounded-[1.5rem] p-5 z-50 pointer-events-auto"
|
||||
>
|
||||
<div className="text-[9px] font-black text-medical-gray-400 uppercase tracking-[0.2em] mb-4 pr-1">تغییر پت یا مدیریت حساب</div>
|
||||
<div className="text-[9px] font-black text-medical-gray-400 uppercase tracking-[0.2em] mb-4 pr-1">{getText('nav_pet_switcher_title', "تغییر پت یا مدیریت حساب")}</div>
|
||||
<div className="space-y-1 mb-4">
|
||||
{pets.map(pet => (
|
||||
<div
|
||||
@ -334,21 +336,21 @@ export default function Header({
|
||||
className="w-full flex items-center gap-3 p-3 text-canina-blue hover:bg-canina-blue/5 rounded-xl transition-all text-[12px] font-black font-vazir"
|
||||
>
|
||||
<PlusCircle className="w-5 h-5" />
|
||||
ثبت همدم (Pet) جدید
|
||||
{getText('nav_register_pet', "ثبت همدم (Pet) جدید")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { onNavigate({ view: "user-dashboard" }); setIsPetSwitcherOpen(false); }}
|
||||
className="w-full flex items-center gap-3 p-3 text-medical-gray-600 hover:bg-medical-gray-50 rounded-xl transition-all text-[12px] font-bold font-vazir"
|
||||
>
|
||||
<Wallet className="w-5 h-5" />
|
||||
کیف پول و سفارشات
|
||||
{getText('nav_wallet_orders', "کیف پول و سفارشات")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { logout(); setIsPetSwitcherOpen(false); }}
|
||||
className="w-full flex items-center gap-3 p-3 text-red-500 hover:bg-red-50 rounded-xl transition-all text-[12px] font-bold font-vazir mt-1"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
خروج از حساب کانینا
|
||||
{getText('nav_logout', "خروج از حساب کانینا")}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
@ -363,7 +365,7 @@ export default function Header({
|
||||
onClick={() => setIsAuthModalOpen(true)}
|
||||
icon={<LogIn className="w-5 h-5" />}
|
||||
>
|
||||
ورود / ثبتنام
|
||||
{getText('nav_login', "ورود / ثبتنام")}
|
||||
</HeaderButton>
|
||||
)}
|
||||
|
||||
@ -374,7 +376,7 @@ export default function Header({
|
||||
badge={getTotalItems()}
|
||||
icon={<ShoppingBag className="w-6 h-6" />}
|
||||
>
|
||||
<div className="text-[8px] font-black tracking-tight whitespace-nowrap mt-0.5">سبد خرید</div>
|
||||
<div className="text-[8px] font-black tracking-tight whitespace-nowrap mt-0.5">{getText('nav_cart', "سبد خرید")}</div>
|
||||
</HeaderButton>
|
||||
</div>
|
||||
</div>
|
||||
@ -405,7 +407,7 @@ export default function Header({
|
||||
<div className="flex items-center justify-between border-b border-medical-gray-100 pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-canina-blue rounded-xl flex items-center justify-center text-white font-bold text-xl italic">C</div>
|
||||
<span className="text-canina-blue font-black text-xl italic font-inter">Canina</span>
|
||||
<span className="text-canina-blue font-black text-xl italic font-sans">Canina</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
@ -423,7 +425,7 @@ export default function Header({
|
||||
onClick={() => setIsMobileSolutionsOpen(!isMobileSolutionsOpen)}
|
||||
className="w-full flex items-center justify-between px-4 py-3 rounded-xl text-sm font-black text-medical-gray-600 hover:bg-medical-gray-50 transition-all font-vazir"
|
||||
>
|
||||
<span>راهکارهای درمانی</span>
|
||||
<span>{getText('nav_solutions', "راهکارهای درمانی")}</span>
|
||||
<ChevronDown className={`w-4 h-4 transition-transform duration-300 ${isMobileSolutionsOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
@ -469,10 +471,10 @@ export default function Header({
|
||||
</div>
|
||||
|
||||
{[
|
||||
{ id: 'shop', label: 'محصولات تخصصی', action: () => onShopNavigate() },
|
||||
{ id: 'wiki', label: 'دانشنامه علمی', action: () => onNavigate('wiki') },
|
||||
{ id: 'blog', label: 'مجله سلامت پت', action: () => onNavigate('blog') },
|
||||
{ id: 'profile', label: 'شناسنامه پتها', action: () => onNavigate({ view: 'profile', subview: 'index' }) }
|
||||
{ id: 'shop', label: getText('nav_products', 'محصولات تخصصی'), action: () => onShopNavigate() },
|
||||
{ id: 'wiki', label: getText('nav_wiki', 'دانشنامه علمی'), action: () => onNavigate('wiki') },
|
||||
{ id: 'blog', label: getText('nav_blog', 'مجله سلامت پت'), action: () => onNavigate('blog') },
|
||||
{ id: 'profile', label: getText('nav_pet_profiles', 'شناسنامه پتها'), action: () => onNavigate({ view: 'profile', subview: 'index' }) }
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
|
||||
@ -138,9 +138,9 @@ export default function Hero({ banners = [] }: { banners?: any[] }) {
|
||||
>
|
||||
<div className="aspect-square bg-gradient-to-br from-slate-200 to-slate-300 rounded-[2rem] overflow-hidden border-4 border-white shadow-2xl relative">
|
||||
<img
|
||||
src={banners.length > 0 && banners[0].imageUrl ? banners[0].imageUrl : "https://images.unsplash.com/photo-1583337130417-3346a1be7dee?w=800&q=80"}
|
||||
alt={banners.length > 0 ? banners[0].title : "German Veterinary Expert"}
|
||||
className="w-full h-full object-cover grayscale-[20%] hover:grayscale-0 transition-all duration-700"
|
||||
src={banners.length > 0 && banners[0].imageUrl ? banners[0].imageUrl : "/assets/images/hero-section-image.png"}
|
||||
alt={banners.length > 0 ? banners[0].title : "Canina Pharma Germany"}
|
||||
className="w-full h-full object-cover"
|
||||
loading="eager"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Product, IngredientInfo } from "../lib/data/products";
|
||||
import { motion } from "motion/react";
|
||||
import { FlaskConical, CheckCircle2, ChevronRight, ChevronLeft, Beaker } from "lucide-react";
|
||||
import { FlaskConical, CheckCircle2, ChevronRight, ChevronLeft, Beaker, Search } from "lucide-react";
|
||||
import { useSettingsStore } from "../lib/store/settingsStore";
|
||||
import { productService } from "../lib/services/productService";
|
||||
|
||||
@ -13,9 +13,10 @@ export default function IngredientWiki() {
|
||||
const texts = useSettingsStore(state => state.texts);
|
||||
const [ingredientsWiki, setIngredientsWiki] = useState<IngredientInfo[]>([]);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
productService.getProducts().then(data => setProducts(data));
|
||||
productService.getProducts({ limit: 999 }).then(res => setProducts(res.data));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -76,10 +77,36 @@ export default function IngredientWiki() {
|
||||
<p className="mt-6 text-lg text-medical-gray-500 max-w-2xl leading-relaxed">
|
||||
ما از مواد اولیهای استفاده میکنیم که در سطح دارویی فرآوری شدهاند. در اینجا با قدرت بیولوژیک این ترکیبات آشنا شوید.
|
||||
</p>
|
||||
<div className="relative w-full max-w-md mt-8">
|
||||
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-medical-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="جستجوی ماده مؤثره..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-full py-3 pr-11 pl-4 text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
const filtered = ingredientsWiki.filter(ing => {
|
||||
if (!searchQuery.trim()) return true;
|
||||
const q = searchQuery.trim().toLowerCase();
|
||||
return ing.name.toLowerCase().includes(q) || ing.description.toLowerCase().includes(q);
|
||||
});
|
||||
if (filtered.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<Search className="w-12 h-12 text-medical-gray-300 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-black text-medical-gray-900 mb-2">مادهای یافت نشد</h3>
|
||||
<p className="text-medical-gray-400 font-bold">با عبارت جستجو شده ماده مؤثرهای مطابقت ندارد.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="space-y-32">
|
||||
{ingredientsWiki.map((ing, idx) => {
|
||||
{filtered.map((ing, idx) => {
|
||||
const hasIngredient = products.filter(p =>
|
||||
p.main_ingredients?.some(mi => mi.toLowerCase().includes(ing.name.toLowerCase())) ||
|
||||
p.main_ingredients?.some(mi => mi.toLowerCase().includes(ing.id.replace('-', ' ')))
|
||||
@ -133,15 +160,21 @@ export default function IngredientWiki() {
|
||||
{hasIngredient.length > 0 ? hasIngredient.map(p => (
|
||||
<div
|
||||
key={p.id}
|
||||
onClick={() => router.push(`/shop/${p.id}`)}
|
||||
onClick={() => router.push(`/shop/${p.slug || p.id}`)}
|
||||
className="bg-white rounded-[2.5rem] p-6 shadow-xl hover:scale-105 transition-transform cursor-pointer group"
|
||||
>
|
||||
<img src={p.image} alt={p.name} className="w-24 h-24 object-contain mx-auto mb-4" referrerPolicy="no-referrer" />
|
||||
<h4 className="text-sm font-black text-center text-medical-gray-900 group-hover:text-canina-blue transition-colors px-2">{p.name}</h4>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="col-span-full py-12 text-center text-medical-gray-400 font-bold italic">
|
||||
اطلاعات محصولات مرتبط به زودی اضافه خواهد شد.
|
||||
<div className="col-span-full py-8 text-center">
|
||||
<p className="text-medical-gray-400 font-bold italic mb-3">محصولی با این ماده مؤثره یافت نشد.</p>
|
||||
<button
|
||||
onClick={() => router.push('/shop')}
|
||||
className="text-canina-blue font-black text-sm hover:underline"
|
||||
>
|
||||
مشاهده همه محصولات
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -154,6 +187,8 @@ export default function IngredientWiki() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Quality Badge */}
|
||||
<div className="mt-32 p-12 bg-medical-gray-900 rounded-[4rem] text-center text-white relative overflow-hidden">
|
||||
|
||||
@ -49,8 +49,8 @@ export default function PetProfile({ initialView, advisorNeed }: {
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
productService.getProducts()
|
||||
.then(setProducts)
|
||||
productService.getProducts({ limit: 999 })
|
||||
.then(res => setProducts(res.data))
|
||||
.catch(err => console.error("Error fetching products in PetProfile:", err));
|
||||
}, []);
|
||||
|
||||
|
||||
@ -79,8 +79,9 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
||||
const [allProducts, setAllProducts] = useState<Product[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
productService.getProducts()
|
||||
.then((products) => {
|
||||
productService.getProducts({ limit: 999 })
|
||||
.then((res) => {
|
||||
const products = res.data;
|
||||
setAllProducts(products);
|
||||
const found = products.find(p => p.slug === productSlug || p.id === productSlug);
|
||||
setProduct(found || null);
|
||||
@ -225,7 +226,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
||||
</div>
|
||||
|
||||
{/* Key Benefits Section */}
|
||||
{fullProduct.keyBenefits?.length > 0 && (
|
||||
{fullProduct.keyBenefits && fullProduct.keyBenefits.length > 0 && (
|
||||
<section className="space-y-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-canina-blue text-white rounded-2xl flex items-center justify-center shadow-lg">
|
||||
@ -383,7 +384,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
{product.faqs?.length > 0 && (
|
||||
{product.faqs && product.faqs.length > 0 && (
|
||||
<section className="pt-10">
|
||||
<div className="flex items-center gap-4 mb-10">
|
||||
<div className="w-12 h-12 bg-blue-100 text-canina-blue rounded-2xl flex items-center justify-center">
|
||||
@ -500,7 +501,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
||||
</div>
|
||||
|
||||
{/* expected results box */}
|
||||
{product.expectedResults?.length > 0 && (
|
||||
{product.expectedResults && product.expectedResults.length > 0 && (
|
||||
<div className="bg-white rounded-[2.5rem] border border-medical-gray-200 p-8">
|
||||
<h4 className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-6 font-vazir">تغییرات قابل انتظار</h4>
|
||||
<div className="space-y-4">
|
||||
|
||||
@ -15,8 +15,8 @@ export default function SearchResultsPage({ query }: { query: string }) {
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
productService.getProducts()
|
||||
.then(setProducts)
|
||||
productService.getProducts({ limit: 999 })
|
||||
.then(res => setProducts(res.data))
|
||||
.catch(err => console.error("Error fetching products in SearchResultsPage:", err));
|
||||
}, []);
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
|
||||
const setLoginModalOpen = useUIStore(state => state.setLoginModalOpen);
|
||||
const { addPet, setActivePet } = usePetStore();
|
||||
const texts = useSettingsStore(state => state.texts);
|
||||
const getText = useSettingsStore(state => state.getText);
|
||||
const medicalOptions = React.useMemo(() => {
|
||||
const jsonStr = texts['medical_options'];
|
||||
if (jsonStr) {
|
||||
@ -98,10 +99,10 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-canina-blue/5 rounded-full mb-6"
|
||||
>
|
||||
<Sparkle className="w-4 h-4 text-canina-blue" />
|
||||
<span className="text-canina-blue text-xs font-black uppercase tracking-widest font-vazir">تکنولوژی پایش هوشمند</span>
|
||||
<span className="text-canina-blue text-xs font-black uppercase tracking-widest font-vazir">{getText('advisor_badge', "تکنولوژی پایش هوشمند")}</span>
|
||||
</motion.div>
|
||||
<h2 className="text-4xl lg:text-5xl font-black text-medical-gray-900 mb-4 font-vazir italic">دستیار سلامت کانینا</h2>
|
||||
<p className="text-medical-gray-600 font-bold text-lg font-vazir">فقط چند قدم تا صدور شناسنامه هوشمند و دریافت پیشنهادات تخصصی</p>
|
||||
<h2 className="text-4xl lg:text-5xl font-black text-medical-gray-900 mb-4 font-vazir italic">{getText('advisor_title', "دستیار سلامت کانینا")}</h2>
|
||||
<p className="text-medical-gray-600 font-bold text-lg font-vazir">{getText('advisor_subtitle', "فقط چند قدم تا صدور شناسنامه هوشمند و دریافت پیشنهادات تخصصی")}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-medical-gray-50 rounded-[3.5rem] border border-medical-gray-100 p-8 lg:p-12 shadow-2xl shadow-medical-gray-200/50 relative overflow-hidden">
|
||||
@ -125,8 +126,8 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
|
||||
className="space-y-8"
|
||||
>
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-black text-medical-gray-900 mb-2 font-vazir uppercase italic tracking-tight">گام اول: هویت بصری</h3>
|
||||
<p className="text-medical-gray-600 font-bold font-vazir">همدم شما رو با چه اسمی صدا میزنید؟</p>
|
||||
<h3 className="text-2xl font-black text-medical-gray-900 mb-2 font-vazir uppercase italic tracking-tight">{getText('advisor_step1_title', "گام اول: هویت بصری")}</h3>
|
||||
<p className="text-medical-gray-600 font-bold font-vazir">{getText('advisor_step1_desc', "همدم شما رو با چه اسمی صدا میزنید؟")}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@ -148,22 +149,22 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-medical-gray-600 pr-2">نام پت</label>
|
||||
<label className="text-xs font-black text-medical-gray-600 pr-2">{getText('advisor_pet_name_label', "نام پت")}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="لوسی، تدی..."
|
||||
placeholder={getText('advisor_pet_name_placeholder', "لوسی، تدی...")}
|
||||
className="w-full bg-white border border-medical-gray-300 rounded-2xl py-4 px-6 focus:ring-2 focus:ring-canina-blue/30 focus-visible:ring-4 focus-visible:ring-canina-blue/40 outline-none font-black text-lg font-vazir placeholder-medical-gray-500 text-medical-gray-900"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-black text-medical-gray-600 pr-2">نژاد</label>
|
||||
<label className="text-xs font-black text-medical-gray-600 pr-2">{getText('advisor_pet_breed_label', "نژاد")}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={breed}
|
||||
onChange={e => setBreed(e.target.value)}
|
||||
placeholder="ژرمن، پرشین..."
|
||||
placeholder={getText('advisor_pet_breed_placeholder', "ژرمن، پرشین...")}
|
||||
className="w-full bg-white border border-medical-gray-300 rounded-2xl py-4 px-6 focus:ring-2 focus:ring-canina-blue/30 focus-visible:ring-4 focus-visible:ring-canina-blue/40 outline-none font-bold font-vazir placeholder-medical-gray-500 text-medical-gray-900"
|
||||
/>
|
||||
</div>
|
||||
@ -174,12 +175,12 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
|
||||
disabled={!name || !breed}
|
||||
className="w-full py-6 rounded-3xl font-black text-lg flex items-center justify-center gap-3 font-vazir min-h-[48px] transition-all focus-visible:ring-4 focus-visible:ring-canina-blue/30 focus-visible:outline-none disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:shadow-none disabled:border-2 disabled:border-gray-400 bg-medical-gray-900 text-white hover:bg-canina-blue shadow-xl shadow-medical-gray-900/10"
|
||||
>
|
||||
ثبت هویت و ادامه
|
||||
{getText('advisor_submit_btn', "ثبت هویت و ادامه")}
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
{(!name || !breed) && (
|
||||
<p className="text-center text-xs font-black text-amber-600 mt-2 font-vazir animate-pulse">
|
||||
⚠️ لطفاً ابتدا نام و نژاد پت را در بالا وارد کنید.
|
||||
{getText('advisor_validation_warning', "⚠️ لطفاً ابتدا نام و نژاد پت را در بالا وارد کنید.")}
|
||||
</p>
|
||||
)}
|
||||
</motion.div>
|
||||
@ -194,13 +195,13 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
|
||||
className="space-y-8"
|
||||
>
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-black text-medical-gray-900 mb-2 font-vazir uppercase italic tracking-tight">گام دوم: پایش فیزیکی</h3>
|
||||
<p className="text-medical-gray-600 font-bold font-vazir">اطلاعات فیزیکی دقیق به دوزبندی صحیح مکملها کمک میکند</p>
|
||||
<h3 className="text-2xl font-black text-medical-gray-900 mb-2 font-vazir uppercase italic tracking-tight">{getText('advisor_step2_title', "گام دوم: پایش فیزیکی")}</h3>
|
||||
<p className="text-medical-gray-600 font-bold font-vazir">{getText('advisor_step2_desc', "اطلاعات فیزیکی دقیق به دوزبندی صحیح مکملها کمک میکند")}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<label className="block text-center text-sm font-black text-medical-gray-800 font-vazir">سن حیوان (سال)</label>
|
||||
<label className="block text-center text-sm font-black text-medical-gray-800 font-vazir">{getText('advisor_age_label', "سن حیوان (سال)")}</label>
|
||||
<div className="flex items-center gap-4 bg-white rounded-3xl p-2 border-2 border-medical-gray-200">
|
||||
<button onClick={() => setAge(Math.max(0, age - 1))} className="w-12 h-12 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white focus-visible:ring-2 focus-visible:ring-canina-blue focus-visible:outline-none transition-all shadow-sm font-black text-lg">-</button>
|
||||
<span className="flex-1 text-center text-2xl font-black text-canina-blue font-vazir">{toPersian(age.toString())} <span className="text-sm font-bold text-medical-gray-400">سال</span></span>
|
||||
@ -208,7 +209,7 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<label className="block text-center text-sm font-black text-medical-gray-800 font-vazir">وزن حیوان (کیلوگرم)</label>
|
||||
<label className="block text-center text-sm font-black text-medical-gray-800 font-vazir">{getText('advisor_weight_label', "وزن حیوان (کیلوگرم)")}</label>
|
||||
<div className="flex items-center gap-4 bg-white rounded-3xl p-2 border-2 border-medical-gray-200">
|
||||
<button onClick={() => setWeight(Math.max(1, weight - 1))} className="w-12 h-12 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white focus-visible:ring-2 focus-visible:ring-canina-blue focus-visible:outline-none transition-all shadow-sm font-black text-lg">-</button>
|
||||
<span className="flex-1 text-center text-2xl font-black text-canina-blue font-vazir">{toPersian(weight.toString())} <span className="text-sm font-bold text-medical-gray-400">kg</span></span>
|
||||
@ -218,7 +219,7 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="block text-center text-xs font-black text-medical-gray-600 uppercase tracking-widest font-vazir">سطح فعالیت روزانه</label>
|
||||
<label className="block text-center text-xs font-black text-medical-gray-600 uppercase tracking-widest font-vazir">{getText('advisor_activity_label', "سطح فعالیت روزانه")}</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{["کم", "متوسط", "زیاد"].map(level => (
|
||||
<button
|
||||
@ -238,13 +239,13 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
|
||||
className="flex-1 py-6 bg-medical-gray-100 text-medical-gray-600 rounded-3xl font-black text-lg hover:bg-medical-gray-200 focus-visible:ring-4 focus-visible:ring-canina-blue/30 focus-visible:outline-none transition-all flex items-center justify-center gap-3 font-vazir min-h-[48px]"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
قبلی
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="flex-[2] py-6 bg-medical-gray-900 text-white rounded-3xl font-black text-lg hover:bg-canina-blue focus-visible:ring-4 focus-visible:ring-canina-blue/30 focus-visible:outline-none transition-all shadow-xl shadow-medical-gray-900/10 flex items-center justify-center gap-3 font-vazir min-h-[48px]"
|
||||
>
|
||||
گام نهایی سلامت
|
||||
{getText('advisor_back', "قبلی")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="flex-[2] py-6 bg-medical-gray-900 text-white rounded-3xl font-black text-lg hover:bg-canina-blue focus-visible:ring-4 focus-visible:ring-canina-blue/30 focus-visible:outline-none transition-all shadow-xl shadow-medical-gray-900/10 flex items-center justify-center gap-3 font-vazir min-h-[48px]"
|
||||
>
|
||||
{getText('advisor_next_final', "گام نهایی سلامت")}
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
@ -260,8 +261,8 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
|
||||
className="space-y-8"
|
||||
>
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-black text-medical-gray-900 mb-2 font-vazir uppercase italic tracking-tight">گام آخر: سوابق و حساسیتها</h3>
|
||||
<p className="text-medical-gray-600 font-bold font-vazir">در صورت وجود هر یک از موارد زیر، آن را تیک بزنید</p>
|
||||
<h3 className="text-2xl font-black text-medical-gray-900 mb-2 font-vazir uppercase italic tracking-tight">{getText('advisor_step3_title', "گام آخر: سوابق و حساسیتها")}</h3>
|
||||
<p className="text-medical-gray-600 font-bold font-vazir">{getText('advisor_step3_desc', "در صورت وجود هر یک از موارد زیر، آن را تیک بزنید")}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@ -289,13 +290,13 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
|
||||
className="flex-1 py-6 bg-medical-gray-100 text-medical-gray-600 rounded-3xl font-black text-lg hover:bg-medical-gray-200 focus-visible:ring-4 focus-visible:ring-canina-blue/30 focus-visible:outline-none transition-all flex items-center justify-center gap-3 font-vazir min-h-[48px]"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
قبلی
|
||||
{getText('advisor_back', "قبلی")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="flex-[2] py-6 bg-canina-blue text-white rounded-3xl font-black text-lg hover:bg-indigo-700 focus-visible:ring-4 focus-visible:ring-canina-blue/30 focus-visible:outline-none transition-all shadow-xl shadow-canina-blue/20 flex items-center justify-center gap-3 font-vazir min-h-[48px]"
|
||||
>
|
||||
تکمیل و صدور شناسنامه
|
||||
{getText('advisor_complete', "تکمیل و صدور شناسنامه")}
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@ import React, { useState } from "react";
|
||||
import { Play, PlayCircle, Star, ShieldCheck, X } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSettingsStore } from "../lib/store/settingsStore";
|
||||
|
||||
const VIDEOS = [
|
||||
{
|
||||
@ -36,6 +37,7 @@ const VIDEOS = [
|
||||
|
||||
export default function VetGallery({ testimonials = [] }: { testimonials?: any[] }) {
|
||||
const router = useRouter();
|
||||
const getText = useSettingsStore(state => state.getText);
|
||||
const displayItems = testimonials.length > 0 ? testimonials : VIDEOS;
|
||||
const [selectedVideo, setSelectedVideo] = useState<any>(null);
|
||||
|
||||
@ -46,20 +48,20 @@ export default function VetGallery({ testimonials = [] }: { testimonials?: any[]
|
||||
<div className="max-w-2xl">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 bg-canina-blue/10 text-canina-blue rounded-full text-[10px] font-black uppercase tracking-widest mb-4">
|
||||
<Star className="w-3 h-3" />
|
||||
Vet Insights
|
||||
{getText('videos_badge', "Vet Insights")}
|
||||
</div>
|
||||
<h2 className="text-4xl md:text-5xl font-black text-medical-gray-900 italic leading-tight">
|
||||
مشاوره ویدئویی <span className="text-canina-blue underline decoration-4 decoration-canina-blue/20 underline-offset-8">دوست دامپزشک پت شما</span>
|
||||
{getText('videos_title', "مشاوره ویدئویی")} <span className="text-canina-blue underline decoration-4 decoration-canina-blue/20 underline-offset-8">{getText('videos_title_highlight', "دوست دامپزشک پت شما")}</span>
|
||||
</h2>
|
||||
<p className="text-lg text-medical-gray-500 font-medium mt-6">
|
||||
متخصصین آلمانی کانینا مستقیماً با شما صحبت میکنند. نحوه مصرف، مزایای رقابتی و دانش تخصصی پشت هر محصول.
|
||||
{getText('videos_desc', "متخصصین آلمانی کانینا مستقیماً با شما صحبت میکنند. نحوه مصرف، مزایای رقابتی و دانش تخصصی پشت هر محصول.")}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push('/videos')}
|
||||
className="inline-flex items-center gap-2 text-canina-blue font-bold text-sm hover:translate-x-1 transition-transform rtl:hover:-translate-x-1"
|
||||
>
|
||||
مشاهده همه ویدئوها
|
||||
{getText('videos_view_all', "مشاهده همه ویدئوها")}
|
||||
<Play className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -77,18 +77,25 @@ export class ProductService {
|
||||
category?: string;
|
||||
petType?: PetType | "all";
|
||||
query?: string;
|
||||
}): Promise<Product[]> {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): Promise<{ data: Product[]; meta: { total: number; page: number; lastPage: number; limit: number } }> {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.category && filters.category !== "all") params.append('category', filters.category);
|
||||
if (filters?.petType && filters.petType !== "all") params.append('petType', filters.petType);
|
||||
if (filters?.query) params.append('search', filters.query);
|
||||
params.append('page', String(filters?.page || 1));
|
||||
params.append('limit', String(filters?.limit || 12));
|
||||
|
||||
try {
|
||||
const response = await api.get(`/products?${params.toString()}`);
|
||||
return response.data.data.map((item: any) => this.mapBackendToFrontend(item));
|
||||
return {
|
||||
data: response.data.data.map((item: any) => this.mapBackendToFrontend(item)),
|
||||
meta: response.data.meta,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[ProductService] Failed to fetch products:", error);
|
||||
return [];
|
||||
return { data: [], meta: { total: 0, page: 1, lastPage: 0, limit: 12 } };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
frontend/application/public/assets/images/hero-section-image.png
Normal file
BIN
frontend/application/public/assets/images/hero-section-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 MiB |
BIN
frontend/application/public/fonts/VazirmatnVariable.woff2
Normal file
BIN
frontend/application/public/fonts/VazirmatnVariable.woff2
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user