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:
parsa aghaei 2026-07-11 17:19:55 +03:30
parent ab7ed279c8
commit 1a0c98a34e
26 changed files with 653 additions and 132 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ coverage/
*.log *.log
.env* .env*
!.env.example !.env.example
/fonts/
/backend/uploads/

View File

@ -1,6 +1,7 @@
[ [
{ {
"name": "Canina Ballaststoff Mix", "name": "Canina Ballaststoff Mix",
"category_slug": "immune",
"description": "فیبر میکس کانینا حاوی منابع فیبر گیاهی است که به طور روزانه رژیم غذایی خام پت را تقویت می کند. این فیبرها به عنوان پری بیوتیک برای فلور پایدار روده مهم هستند. حرکت طبیعی روده را تحریک کرده، مدفوع را حجیم می کند و تاثیر مثبتی بر تخلیه غدد مقعدی دارد. سیستم ایمنی روده را تقویت کرده و انرژی باکتری های روده بزرگ را تامین می کند. بدون غلات و پروتئین حیوانی بوده و برای آلرژی غذایی مناسب است.", "description": "فیبر میکس کانینا حاوی منابع فیبر گیاهی است که به طور روزانه رژیم غذایی خام پت را تقویت می کند. این فیبرها به عنوان پری بیوتیک برای فلور پایدار روده مهم هستند. حرکت طبیعی روده را تحریک کرده، مدفوع را حجیم می کند و تاثیر مثبتی بر تخلیه غدد مقعدی دارد. سیستم ایمنی روده را تقویت کرده و انرژی باکتری های روده بزرگ را تامین می کند. بدون غلات و پروتئین حیوانی بوده و برای آلرژی غذایی مناسب است.",
"ingredients": "تفاله سیب، پودر یونجه سبز، فیبر نخود فرنگی، تفاله هویج، کیک بذر کتان، کمپلکس الیگوساکارید و بتاگلوکان، مخمر آبجو، کنگر فرنگی، خاک دیاتومه، روغن کلزا، کربنات کلسیم. مواد تشکیل دهنده: پروتئین خام 11.9%، چربی خام 6.3%، فیبر خام 27.3%.", "ingredients": "تفاله سیب، پودر یونجه سبز، فیبر نخود فرنگی، تفاله هویج، کیک بذر کتان، کمپلکس الیگوساکارید و بتاگلوکان، مخمر آبجو، کنگر فرنگی، خاک دیاتومه، روغن کلزا، کربنات کلسیم. مواد تشکیل دهنده: پروتئین خام 11.9%، چربی خام 6.3%، فیبر خام 27.3%.",
"usage_and_dosage": "مناسب برای توله سگ ها، بچه گربه ها و حیوانات مسن، باردار و شیرده. پودر را با کمی آب گرم مخلوط کنید. مقدار مصرف: 1 گرم به ازای هر کیلوگرم وزن بدن، حداکثر 15 گرم در روز.", "usage_and_dosage": "مناسب برای توله سگ ها، بچه گربه ها و حیوانات مسن، باردار و شیرده. پودر را با کمی آب گرم مخلوط کنید. مقدار مصرف: 1 گرم به ازای هر کیلوگرم وزن بدن، حداکثر 15 گرم در روز.",
@ -10,6 +11,7 @@
}, },
{ {
"name": "Canina CANHYDROX GAG", "name": "Canina CANHYDROX GAG",
"category_slug": "joints",
"description": "ویژه سگ های بزرگ و سنگین وزن برای درمان بافت همبند، مفاصل، استخوان ها، تاندون ها و غضروف. ایده آل برای سگ های در مرحله رشد، مسن و سگ های با عملکرد بالا. از تشکیل کالوس و استحکام استخوان پشتیبانی کرده و ظرفیت بافر مفاصل را بهبود می بخشد.", "description": "ویژه سگ های بزرگ و سنگین وزن برای درمان بافت همبند، مفاصل، استخوان ها، تاندون ها و غضروف. ایده آل برای سگ های در مرحله رشد، مسن و سگ های با عملکرد بالا. از تشکیل کالوس و استحکام استخوان پشتیبانی کرده و ظرفیت بافر مفاصل را بهبود می بخشد.",
"ingredients": "30% تری کلسیم فسفات، هیدروکسی آپاتیت، مخمر آبجو، 10% پودر صدف سبز، جلبک دریایی. ویتامین سی، ویتامین ایی، سلنیوم و متیونین.", "ingredients": "30% تری کلسیم فسفات، هیدروکسی آپاتیت، مخمر آبجو، 10% پودر صدف سبز، جلبک دریایی. ویتامین سی، ویتامین ایی، سلنیوم و متیونین.",
"usage_and_dosage": "سگ های جوان از 8 هفتگی: برای پیشگیری 2 قرص و برای اندام های بدشکل 4 قرص به ازای هر 10 کیلوگرم تا 15 ماهگی. سگ های مسن و عملکردی: 4 قرص به ازای هر 10 کیلوگرم. پس از شکستگی: 4 قرص برای دوره نقاهت.", "usage_and_dosage": "سگ های جوان از 8 هفتگی: برای پیشگیری 2 قرص و برای اندام های بدشکل 4 قرص به ازای هر 10 کیلوگرم تا 15 ماهگی. سگ های مسن و عملکردی: 4 قرص به ازای هر 10 کیلوگرم. پس از شکستگی: 4 قرص برای دوره نقاهت.",
@ -19,6 +21,7 @@
}, },
{ {
"name": "Canina Eierschalenpulver", "name": "Canina Eierschalenpulver",
"category_slug": "energy",
"description": "پودر پوسته تخم مرغ برای تامین کلسیم، سلامت دندان ها و فعالیت عضلات. جایگزین پودر استخوان برای سگ های دارای آلرژی به پروتئین گوشت. مناسب برای تکمیل وعده های غذایی گوشتی خام (B.A.R.F).", "description": "پودر پوسته تخم مرغ برای تامین کلسیم، سلامت دندان ها و فعالیت عضلات. جایگزین پودر استخوان برای سگ های دارای آلرژی به پروتئین گوشت. مناسب برای تکمیل وعده های غذایی گوشتی خام (B.A.R.F).",
"ingredients": "100% پوسته تخم مرغ آسیاب شده. خاکستر خام 96%، کلسیم 36%، فسفر 0.1%.", "ingredients": "100% پوسته تخم مرغ آسیاب شده. خاکستر خام 96%، کلسیم 36%، فسفر 0.1%.",
"usage_and_dosage": "0.2 گرم به ازای هر کیلوگرم وزن بدن، حداکثر 16 گرم برای هر حیوان در روز.", "usage_and_dosage": "0.2 گرم به ازای هر کیلوگرم وزن بدن، حداکثر 16 گرم برای هر حیوان در روز.",
@ -28,6 +31,7 @@
}, },
{ {
"name": "Canina FLEXAN", "name": "Canina FLEXAN",
"category_slug": "joints",
"description": "پشتیبانی از سیستم اسکلتی عضلانی، مفاصل متحرک و تناسب اندام. پپتیدهای کلاژن غضروف را بازسازی کرده و پودر صدف لب سبز بافت همبند را تقویت می کند.", "description": "پشتیبانی از سیستم اسکلتی عضلانی، مفاصل متحرک و تناسب اندام. پپتیدهای کلاژن غضروف را بازسازی کرده و پودر صدف لب سبز بافت همبند را تقویت می کند.",
"ingredients": "90% کلاژن، 10% پودر گوشت صدف لب سبز. پروتئین خام 94.2%.", "ingredients": "90% کلاژن، 10% پودر گوشت صدف لب سبز. پروتئین خام 94.2%.",
"usage_and_dosage": "تغذیه روزانه برای توله سگ ها تا مسن. 2 گرم به ازای هر 10 کیلوگرم وزن بدن، حداکثر 15 گرم در روز.", "usage_and_dosage": "تغذیه روزانه برای توله سگ ها تا مسن. 2 گرم به ازای هر 10 کیلوگرم وزن بدن، حداکثر 15 گرم در روز.",
@ -37,6 +41,7 @@
}, },
{ {
"name": "Canina HERZ VITAL", "name": "Canina HERZ VITAL",
"category_slug": "energy",
"description": "برای سگ ها و گربه های مسن دارای نارسایی قلبی. ال کارنیتین بر تامین انرژی قلب و عضلات تاثیر گذاشته و نشاط و رفاه حیوان را افزایش می دهد.", "description": "برای سگ ها و گربه های مسن دارای نارسایی قلبی. ال کارنیتین بر تامین انرژی قلب و عضلات تاثیر گذاشته و نشاط و رفاه حیوان را افزایش می دهد.",
"ingredients": "دکستروز، مخمر غیر فعال، زالزالک. حاوی ال کارنیتین.", "ingredients": "دکستروز، مخمر غیر فعال، زالزالک. حاوی ال کارنیتین.",
"usage_and_dosage": "نصف قاشق چایخوری به ازای هر 10 کیلوگرم وزن بدن، حداکثر 3 قاشق چایخوری. به مدت 6 ماه همراه غذا تجویز شود.", "usage_and_dosage": "نصف قاشق چایخوری به ازای هر 10 کیلوگرم وزن بدن، حداکثر 3 قاشق چایخوری. به مدت 6 ماه همراه غذا تجویز شود.",
@ -46,6 +51,7 @@
}, },
{ {
"name": "Canina IMMUN-BOOSTER Paste", "name": "Canina IMMUN-BOOSTER Paste",
"category_slug": "immune",
"description": "بازیابی و نقاهت تغذیه ای با چگالی انرژی بالا. ایمونوگلوبولین های آغوز سیستم دفاعی را تقویت کرده و برای نقاهت پس از زایمان، تمرین و استرس عالی است. انتروکوک فاسیوم فلور روده را سالم نگه می دارد.", "description": "بازیابی و نقاهت تغذیه ای با چگالی انرژی بالا. ایمونوگلوبولین های آغوز سیستم دفاعی را تقویت کرده و برای نقاهت پس از زایمان، تمرین و استرس عالی است. انتروکوک فاسیوم فلور روده را سالم نگه می دارد.",
"ingredients": "روغن های گیاهی، 20% پودر آغوز، شکر انگور. ویتامین آ، د3، ایی و تثبیت کننده فلور روده.", "ingredients": "روغن های گیاهی، 20% پودر آغوز، شکر انگور. ویتامین آ، د3، ایی و تثبیت کننده فلور روده.",
"usage_and_dosage": "توله ها 0.25 میلی لیتر، سگ های متوسط 0.5-1 میلی لیتر، سگ های بزرگ 1-4 میلی لیتر در روز تا زمان بهبودی کامل.", "usage_and_dosage": "توله ها 0.25 میلی لیتر، سگ های متوسط 0.5-1 میلی لیتر، سگ های بزرگ 1-4 میلی لیتر در روز تا زمان بهبودی کامل.",
@ -55,6 +61,7 @@
}, },
{ {
"name": "Canina Katzenmilch", "name": "Canina Katzenmilch",
"category_slug": "nutrition",
"description": "غذای کامل جایگزین شیر مادر با ویتامین ها و مواد معدنی. مناسب بچه گربه های بی مادر از 1 روزگی، گربه های مسن، بیمار و شیرده.", "description": "غذای کامل جایگزین شیر مادر با ویتامین ها و مواد معدنی. مناسب بچه گربه های بی مادر از 1 روزگی، گربه های مسن، بیمار و شیرده.",
"ingredients": "لاکتوز، پودر آب پنیر، چربی نارگیل، روغن ماهی، پروتئین 30%. غنی از ویتامین ها و 100 میلی گرم تورین.", "ingredients": "لاکتوز، پودر آب پنیر، چربی نارگیل، روغن ماهی، پروتئین 30%. غنی از ویتامین ها و 100 میلی گرم تورین.",
"usage_and_dosage": "1 قسمت شیر با 3 قسمت آب ولرم مخلوط شود. به بچه گربه های 1 تا 14 روزه هر 2 ساعت تغذیه شود.", "usage_and_dosage": "1 قسمت شیر با 3 قسمت آب ولرم مخلوط شود. به بچه گربه های 1 تا 14 روزه هر 2 ساعت تغذیه شود.",
@ -64,6 +71,7 @@
}, },
{ {
"name": "Canina Lachsöl", "name": "Canina Lachsöl",
"category_slug": "special-care",
"description": "روغن ماهی سالمون 100% خالص سرشار از اسیدهای چرب امگا 3 (EPA و DHA). متابولیسم را تنظیم کرده و از بازسازی پوست و رشد مو پشتیبانی می کند.", "description": "روغن ماهی سالمون 100% خالص سرشار از اسیدهای چرب امگا 3 (EPA و DHA). متابولیسم را تنظیم کرده و از بازسازی پوست و رشد مو پشتیبانی می کند.",
"ingredients": "100% روغن ماهی سالمون. اسیدهای چرب چند غیراشباع 56%، امگا 3 (14%) و امگا 6 (11%).", "ingredients": "100% روغن ماهی سالمون. اسیدهای چرب چند غیراشباع 56%، امگا 3 (14%) و امگا 6 (11%).",
"usage_and_dosage": "2 میلی لیتر به ازای هر 10 کیلوگرم وزن بدن، حداکثر 10 میلی لیتر در روز مخلوط با خوراک.", "usage_and_dosage": "2 میلی لیتر به ازای هر 10 کیلوگرم وزن بدن، حداکثر 10 میلی لیتر در روز مخلوط با خوراک.",
@ -73,6 +81,7 @@
}, },
{ {
"name": "Canina Marine-Ölmischung PREMIUM", "name": "Canina Marine-Ölmischung PREMIUM",
"category_slug": "special-care",
"description": "ترکیبی هماهنگ از روغن های خوراکی که متابولیسم ارگانیسم را ارتقا می دهد. از کمبود مواد مغذی در رژیم های خام جلوگیری می کند. روغن آرگان و گل رز به عنوان آنتی اکسیدان و بازسازی کننده عمل می کنند.", "description": "ترکیبی هماهنگ از روغن های خوراکی که متابولیسم ارگانیسم را ارتقا می دهد. از کمبود مواد مغذی در رژیم های خام جلوگیری می کند. روغن آرگان و گل رز به عنوان آنتی اکسیدان و بازسازی کننده عمل می کنند.",
"ingredients": "روغن ماهی، روغن گردو، روغن آرگان، روغن دانه گل رز. چربی خام 100%.", "ingredients": "روغن ماهی، روغن گردو، روغن آرگان، روغن دانه گل رز. چربی خام 100%.",
"usage_and_dosage": "قابل افزودن به تمام وعده های غذایی برای بهینه سازی ویتامین ها و اسیدهای چرب امگا.", "usage_and_dosage": "قابل افزودن به تمام وعده های غذایی برای بهینه سازی ویتامین ها و اسیدهای چرب امگا.",
@ -82,6 +91,7 @@
}, },
{ {
"name": "Canina Moortränke", "name": "Canina Moortränke",
"category_slug": "immune",
"description": "عصاره پیت حاوی مواد هومیک برای محافظت از غشاهای مخاطی و سیستم ایمنی روده. ایده آل برای نفخ، اسهال، پس از درمان آنتی بیوتیک، تحریک اشتها و خون سازی.", "description": "عصاره پیت حاوی مواد هومیک برای محافظت از غشاهای مخاطی و سیستم ایمنی روده. ایده آل برای نفخ، اسهال، پس از درمان آنتی بیوتیک، تحریک اشتها و خون سازی.",
"ingredients": "100% پیت، پیت مایع (بدون افزودنی و نگهدارنده).", "ingredients": "100% پیت، پیت مایع (بدون افزودنی و نگهدارنده).",
"usage_and_dosage": "قبل از استفاده تکان دهید. سگ های کوچک 2.5 تا 5 میلی لیتر، سگ های متوسط 10 میلی لیتر، سگ های بزرگتر 15 تا 20 میلی لیتر. از 20 میلی لیتر تجاوز نکند.", "usage_and_dosage": "قبل از استفاده تکان دهید. سگ های کوچک 2.5 تا 5 میلی لیتر، سگ های متوسط 10 میلی لیتر، سگ های بزرگتر 15 تا 20 میلی لیتر. از 20 میلی لیتر تجاوز نکند.",
@ -91,6 +101,7 @@
}, },
{ {
"name": "Canina PETVITAL Arthro-Tabletten", "name": "Canina PETVITAL Arthro-Tabletten",
"category_slug": "joints",
"description": "متابولیسم استخوان را بهبود بخشیده و مفاصل ضعیف را تقویت می کند. عصاره بید و زنجبیل به عنوان آنتی اکسیدان عمل کرده و خاصیت ارتجاعی استخوان را افزایش می دهند.", "description": "متابولیسم استخوان را بهبود بخشیده و مفاصل ضعیف را تقویت می کند. عصاره بید و زنجبیل به عنوان آنتی اکسیدان عمل کرده و خاصیت ارتجاعی استخوان را افزایش می دهند.",
"ingredients": "گیاهان خشک (بید و زنجبیل)، ویتامین ایی، ویتامین ب1، آهن، مس، روی.", "ingredients": "گیاهان خشک (بید و زنجبیل)، ویتامین ایی، ویتامین ب1، آهن، مس، روی.",
"usage_and_dosage": "توله سگ ها: 2 قرص به ازای 10 کیلوگرم برای 1 ماه. سگ های بالغ: 5 قرص به ازای 10 کیلوگرم برای 3 ماه (سالی یکبار تکرار شود).", "usage_and_dosage": "توله سگ ها: 2 قرص به ازای 10 کیلوگرم برای 1 ماه. سگ های بالغ: 5 قرص به ازای 10 کیلوگرم برای 3 ماه (سالی یکبار تکرار شود).",
@ -100,6 +111,7 @@
}, },
{ {
"name": "Canina PETVITAL Bio-Aktivator", "name": "Canina PETVITAL Bio-Aktivator",
"category_slug": "energy",
"description": "حاوی سرم خون گاو برای تحریک اشتها و تسریع بهبودی در دوره نقاهت. رفاه و هماهنگی متابولیسم را بهبود می بخشد.", "description": "حاوی سرم خون گاو برای تحریک اشتها و تسریع بهبودی در دوره نقاهت. رفاه و هماهنگی متابولیسم را بهبود می بخشد.",
"ingredients": "سرم خون گاو، اتانول 164.4 گرم (به عنوان طعم دهنده).", "ingredients": "سرم خون گاو، اتانول 164.4 گرم (به عنوان طعم دهنده).",
"usage_and_dosage": "1 قطره به ازای هر کیلوگرم وزن بدن، حداکثر 20 قطره در روز. قطره ها به مدت 4 هفته داده شوند، سپس 2 هفته استراحت.", "usage_and_dosage": "1 قطره به ازای هر کیلوگرم وزن بدن، حداکثر 20 قطره در روز. قطره ها به مدت 4 هفته داده شوند، سپس 2 هفته استراحت.",
@ -109,6 +121,7 @@
}, },
{ {
"name": "Canina PETVITAL Biotin-Tabs", "name": "Canina PETVITAL Biotin-Tabs",
"category_slug": "special-care",
"description": "دوز بالای بیوتین (ویتامین H) برای جلوگیری از ریزش مو، ایجاد پوشش براق، رنگدانه های قوی و پنجه های محکم.", "description": "دوز بالای بیوتین (ویتامین H) برای جلوگیری از ریزش مو، ایجاد پوشش براق، رنگدانه های قوی و پنجه های محکم.",
"ingredients": "دکستروز، آب پنیر، نشاسته، سلولز. بیوتین 1000000 میکروگرم، ویتامین ب2، اسید نیکوتینیک.", "ingredients": "دکستروز، آب پنیر، نشاسته، سلولز. بیوتین 1000000 میکروگرم، ویتامین ب2، اسید نیکوتینیک.",
"usage_and_dosage": "یک قرص به ازای هر 10 کیلوگرم وزن بدن، حداکثر 5 قرص در روز.", "usage_and_dosage": "یک قرص به ازای هر 10 کیلوگرم وزن بدن، حداکثر 5 قرص در روز.",
@ -118,6 +131,7 @@
}, },
{ {
"name": "Canina PETVITAL ENERGY-GEL", "name": "Canina PETVITAL ENERGY-GEL",
"category_slug": "energy",
"description": "کنسانتره انرژی برای اصلاح سریع کمبودهای مواد مغذی. ایده آل برای دوره نقاهت جهت تحریک اشتها، دوره های رشد، بارداری و شیردهی.", "description": "کنسانتره انرژی برای اصلاح سریع کمبودهای مواد مغذی. ایده آل برای دوره نقاهت جهت تحریک اشتها، دوره های رشد، بارداری و شیردهی.",
"ingredients": "روغن ها، چربی ها، ویتامین ایی، ویتامین ب1، ب2، ب6، ب12.", "ingredients": "روغن ها، چربی ها، ویتامین ایی، ویتامین ب1، ب2، ب6، ب12.",
"usage_and_dosage": "1 قاشق چایخوری (3 میلی لیتر) به ازای 10 کیلوگرم وزن بدن، حداکثر 4 قاشق چایخوری در روز.", "usage_and_dosage": "1 قاشق چایخوری (3 میلی لیتر) به ازای 10 کیلوگرم وزن بدن، حداکثر 4 قاشق چایخوری در روز.",
@ -127,6 +141,7 @@
}, },
{ {
"name": "Canina PETVITAL MINERAL-Tabs", "name": "Canina PETVITAL MINERAL-Tabs",
"category_slug": "energy",
"description": "انواع مختلف کلسیم برای برطرف کردن کمبود و تشکیل بافت استخوانی. عالی برای دوره رشد، بارداری و شیردهی جهت تامین نیاز کلسیم.", "description": "انواع مختلف کلسیم برای برطرف کردن کمبود و تشکیل بافت استخوانی. عالی برای دوره رشد، بارداری و شیردهی جهت تامین نیاز کلسیم.",
"ingredients": "کربنات کلسیم، مخمر آبجو، ویتامین د3 100000 (برای ذخیره کلسیم در استخوان).", "ingredients": "کربنات کلسیم، مخمر آبجو، ویتامین د3 100000 (برای ذخیره کلسیم در استخوان).",
"usage_and_dosage": "سگ های بالغ: 2 قرص به ازای 10 کیلوگرم. باردار و شیرده: 3 قرص به ازای 10 کیلوگرم. مصرف ویتامین D2 همزمان مجاز نیست.", "usage_and_dosage": "سگ های بالغ: 2 قرص به ازای 10 کیلوگرم. باردار و شیرده: 3 قرص به ازای 10 کیلوگرم. مصرف ویتامین D2 همزمان مجاز نیست.",
@ -136,6 +151,7 @@
}, },
{ {
"name": "Canina PETVITAL GAG", "name": "Canina PETVITAL GAG",
"category_slug": "joints",
"description": "تامین گلیکوزآمینوگلیکان ها، تاندون ها، رباط ها و غضروف مفاصل را تثبیت می کند. شامل پودر صدف لب سبز و اسیدهای آمینه برای عملکرد بهینه سیستم حرکتی.", "description": "تامین گلیکوزآمینوگلیکان ها، تاندون ها، رباط ها و غضروف مفاصل را تثبیت می کند. شامل پودر صدف لب سبز و اسیدهای آمینه برای عملکرد بهینه سیستم حرکتی.",
"ingredients": "پودر ماهی، پودر جلبک دریایی، پودر گوشت صدف لب سبز. دی ال متیونین، ویتامین ایی، سلنیوم.", "ingredients": "پودر ماهی، پودر جلبک دریایی، پودر گوشت صدف لب سبز. دی ال متیونین، ویتامین ایی، سلنیوم.",
"usage_and_dosage": "پودر: 1 قاشق چایخوری به ازای 10 کیلوگرم (حداکثر 4 قاشق). قرص: 1 قرص به ازای 10 کیلوگرم (حداکثر 5 قرص).", "usage_and_dosage": "پودر: 1 قاشق چایخوری به ازای 10 کیلوگرم (حداکثر 4 قاشق). قرص: 1 قرص به ازای 10 کیلوگرم (حداکثر 5 قرص).",
@ -147,6 +163,7 @@
}, },
{ {
"name": "Canina PETVITAL VITAMIN-Tabs", "name": "Canina PETVITAL VITAMIN-Tabs",
"category_slug": "energy",
"description": "اصلاح کمبود ویتامین، جلوگیری از خستگی، عدم تناسب و اختلالات رشد. ایده آل برای مادران باردار و شرایط استرس زا.", "description": "اصلاح کمبود ویتامین، جلوگیری از خستگی، عدم تناسب و اختلالات رشد. ایده آل برای مادران باردار و شرایط استرس زا.",
"ingredients": "مخمر آبجو، سلولز، ویتامین های آ، ب1 تا ب12، د3، ایی، کا3.", "ingredients": "مخمر آبجو، سلولز، ویتامین های آ، ب1 تا ب12، د3، ایی، کا3.",
"usage_and_dosage": "1 قرص به ازای هر 10 کیلوگرم وزن بدن در روز.", "usage_and_dosage": "1 قرص به ازای هر 10 کیلوگرم وزن بدن در روز.",
@ -156,6 +173,7 @@
}, },
{ {
"name": "Canina Rinderblut Pulver", "name": "Canina Rinderblut Pulver",
"category_slug": "energy",
"description": "پودر خون گاو، منبع ارزشمند آهن و هموگلوبین برای انتقال اکسیژن. مکمل عالی برای رژیم گوشت خام (B.A.R.F) جهت تامین خون از دست رفته و رشد عضلات. مناسب دوره نقاهت و سگ های ورزشی.", "description": "پودر خون گاو، منبع ارزشمند آهن و هموگلوبین برای انتقال اکسیژن. مکمل عالی برای رژیم گوشت خام (B.A.R.F) جهت تامین خون از دست رفته و رشد عضلات. مناسب دوره نقاهت و سگ های ورزشی.",
"ingredients": "خون کامل گاو خشک شده. پروتئین خام 92%، آهن.", "ingredients": "خون کامل گاو خشک شده. پروتئین خام 92%، آهن.",
"usage_and_dosage": "0.3 گرم به ازای هر کیلوگرم وزن بدن (حداکثر 12 گرم در روز) مخلوط با خوراک خام برای 6 ماه.", "usage_and_dosage": "0.3 گرم به ازای هر کیلوگرم وزن بدن (حداکثر 12 گرم در روز) مخلوط با خوراک خام برای 6 ماه.",
@ -166,6 +184,7 @@
}, },
{ {
"name": "Canina Rinderfett Pulver", "name": "Canina Rinderfett Pulver",
"category_slug": "energy",
"description": "منبع انرژی طبیعی از چربی گوشت گاو آب پز بدون شکر. عالی برای حیوانات لاغر، سگ های بسیار فعال، و جبران کمبود چربی در وعده های خام.", "description": "منبع انرژی طبیعی از چربی گوشت گاو آب پز بدون شکر. عالی برای حیوانات لاغر، سگ های بسیار فعال، و جبران کمبود چربی در وعده های خام.",
"ingredients": "72% چربی گوشت گاو، 27% آب گوشت گاو خشک شده. انرژی: 740 کیلوکالری.", "ingredients": "72% چربی گوشت گاو، 27% آب گوشت گاو خشک شده. انرژی: 740 کیلوکالری.",
"usage_and_dosage": "نصف قاشق چایخوری به ازای هر 10 کیلوگرم وزن بدن، حداکثر 3 قاشق چایخوری در روز همراه غذای مرطوب.", "usage_and_dosage": "نصف قاشق چایخوری به ازای هر 10 کیلوگرم وزن بدن، حداکثر 3 قاشق چایخوری در روز همراه غذای مرطوب.",
@ -175,6 +194,7 @@
}, },
{ {
"name": "Canina Schwarz Kümmel Samen", "name": "Canina Schwarz Kümmel Samen",
"category_slug": "general",
"description": "دانه های زیره سیاه سرشار از اسیدهای چرب غیر اشباع که هضم و متابولیسم را تنظیم می کنند. سیستم دفاعی و عملکرد سگ را تقویت می کند.", "description": "دانه های زیره سیاه سرشار از اسیدهای چرب غیر اشباع که هضم و متابولیسم را تنظیم می کنند. سیستم دفاعی و عملکرد سگ را تقویت می کند.",
"ingredients": "100% دانه های زیره سیاه مصری.", "ingredients": "100% دانه های زیره سیاه مصری.",
"usage_and_dosage": "1 قاشق چایخوری به ازای 10 کیلوگرم وزن بدن، حداکثر 4 قاشق چایخوری مخلوط با آب ولرم. در بارداری استفاده نشود.", "usage_and_dosage": "1 قاشق چایخوری به ازای 10 کیلوگرم وزن بدن، حداکثر 4 قاشق چایخوری مخلوط با آب ولرم. در بارداری استفاده نشود.",
@ -184,6 +204,7 @@
}, },
{ {
"name": "Canina Seealgen / Bio-Seealgenmehl", "name": "Canina Seealgen / Bio-Seealgenmehl",
"category_slug": "special-care",
"description": "جلبک دریایی ارگانیک سرشار از مواد معدنی و اسیدهای آمینه که با ید و روی، رنگدانه های پوست، بینی و پوشش را پشتیبانی می کند. هضم و سیستم ایمنی را بهبود می بخشد.", "description": "جلبک دریایی ارگانیک سرشار از مواد معدنی و اسیدهای آمینه که با ید و روی، رنگدانه های پوست، بینی و پوشش را پشتیبانی می کند. هضم و سیستم ایمنی را بهبود می بخشد.",
"ingredients": "جلبک دریایی حاوی لیزین، سلولز و ویتامین سی.", "ingredients": "جلبک دریایی حاوی لیزین، سلولز و ویتامین سی.",
"usage_and_dosage": "قرص: 2 عدد به ازای 10 کیلوگرم. پودر: نصف قاشق چایخوری به ازای 10 کیلوگرم وزن بدن. جهت تامین نیاز ید.", "usage_and_dosage": "قرص: 2 عدد به ازای 10 کیلوگرم. پودر: نصف قاشق چایخوری به ازای 10 کیلوگرم وزن بدن. جهت تامین نیاز ید.",
@ -195,6 +216,7 @@
}, },
{ {
"name": "Canina TAURIN Für Katzen", "name": "Canina TAURIN Für Katzen",
"category_slug": "special-care",
"description": "اسید آمینه ضروری برای گربه ها که سیستم ایمنی، بینایی و قلب را پشتیبانی می کند. کمبود آن باعث نابینایی و اختلالات عصبی می شود.", "description": "اسید آمینه ضروری برای گربه ها که سیستم ایمنی، بینایی و قلب را پشتیبانی می کند. کمبود آن باعث نابینایی و اختلالات عصبی می شود.",
"ingredients": "مخمر آبجو، تورین 700 گرم.", "ingredients": "مخمر آبجو، تورین 700 گرم.",
"usage_and_dosage": "گربه تا 6 کیلو: نصف قاشق چایخوری. بالای 6 کیلو و باردار/شیرده: 1 قاشق چایخوری در روز.", "usage_and_dosage": "گربه تا 6 کیلو: نصف قاشق چایخوری. بالای 6 کیلو و باردار/شیرده: 1 قاشق چایخوری در روز.",
@ -204,6 +226,7 @@
}, },
{ {
"name": "Canina Velox Gelenkenergie", "name": "Canina Velox Gelenkenergie",
"category_slug": "joints",
"description": "کنسانتره صدف لب سبز نیوزیلندی برای بازسازی مفاصل، رباط ها و غضروف. افزایش استقامت مفاصل توله ها در حال رشد و سگ های پیر.", "description": "کنسانتره صدف لب سبز نیوزیلندی برای بازسازی مفاصل، رباط ها و غضروف. افزایش استقامت مفاصل توله ها در حال رشد و سگ های پیر.",
"ingredients": "100% پودر گوشت صدف لب سبز.", "ingredients": "100% پودر گوشت صدف لب سبز.",
"usage_and_dosage": "1 قاشق چایخوری به ازای 10 کیلوگرم وزن بدن (حداکثر 2 قاشق) در روز به مدت 4 هفته یا مداوم.", "usage_and_dosage": "1 قاشق چایخوری به ازای 10 کیلوگرم وزن بدن (حداکثر 2 قاشق) در روز به مدت 4 هفته یا مداوم.",
@ -213,6 +236,7 @@
}, },
{ {
"name": "Canina Welpenbrei", "name": "Canina Welpenbrei",
"category_slug": "nutrition",
"description": "فرنی با پروتئین و ویتامین بالا که پرورش سالم توله سگ را از هفته 3 تضمین می کند.", "description": "فرنی با پروتئین و ویتامین بالا که پرورش سالم توله سگ را از هفته 3 تضمین می کند.",
"ingredients": "غلات، گوشت، فرآورده های لبنی، ماهی، پروتئین 27%.", "ingredients": "غلات، گوشت، فرآورده های لبنی، ماهی، پروتئین 27%.",
"usage_and_dosage": "1 قسمت فرنی را با 3 قسمت آب گرم (50 درجه سانتیگراد) مخلوط کنید. مصرف ویتامین D2 همزمان مجاز نیست.", "usage_and_dosage": "1 قسمت فرنی را با 3 قسمت آب گرم (50 درجه سانتیگراد) مخلوط کنید. مصرف ویتامین D2 همزمان مجاز نیست.",
@ -222,6 +246,7 @@
}, },
{ {
"name": "Canina Welpenmilch", "name": "Canina Welpenmilch",
"category_slug": "nutrition",
"description": "شیر کامل جایگزین شیر مادر با انرژی و ویتامین بالا برای توله سگ های بی مادر از روز 1.", "description": "شیر کامل جایگزین شیر مادر با انرژی و ویتامین بالا برای توله سگ های بی مادر از روز 1.",
"ingredients": "لاکتوز، پودر آب پنیر، روغن ماهی، پروتئین 30%، تورین، ویتامین ها.", "ingredients": "لاکتوز، پودر آب پنیر، روغن ماهی، پروتئین 30%، تورین، ویتامین ها.",
"usage_and_dosage": "1 قسمت شیر با 4 قسمت آب 50 درجه مخلوط شود. تغذیه هر 2 ساعت در ابتدا و سپس هر 4 ساعت.", "usage_and_dosage": "1 قسمت شیر با 4 قسمت آب 50 درجه مخلوط شود. تغذیه هر 2 ساعت در ابتدا و سپس هر 4 ساعت.",
@ -231,6 +256,7 @@
}, },
{ {
"name": "Canina PETVITAL Bio-Insect Shocker", "name": "Canina PETVITAL Bio-Insect Shocker",
"category_slug": "special-care",
"description": "اسپری دفع کننده محیطی کک، شپش و کنه. تخم ها و انگل های خارجی را به طور موثر از بین می برد.", "description": "اسپری دفع کننده محیطی کک، شپش و کنه. تخم ها و انگل های خارجی را به طور موثر از بین می برد.",
"ingredients": "15 گرم در لیتر دودکانوئیک اسید، 5 گرم در لیتر عصاره مارگوسا.", "ingredients": "15 گرم در لیتر دودکانوئیک اسید، 5 گرم در لیتر عصاره مارگوسا.",
"usage_and_dosage": "هر 4 تا 6 هفته به صورت اسپری از فاصله 30 سانتی متری روی محل زندگی و خواب حیوان استفاده شود.", "usage_and_dosage": "هر 4 تا 6 هفته به صورت اسپری از فاصله 30 سانتی متری روی محل زندگی و خواب حیوان استفاده شود.",
@ -241,6 +267,7 @@
}, },
{ {
"name": "Canina Mikrosilber Zahngel", "name": "Canina Mikrosilber Zahngel",
"category_slug": "special-care",
"description": "ژل دندانی با میکروسیلور که پلاک، جرم و التهاب لثه را هدف قرار می دهد. بیوفلاونوئیدها از باکتری های پلاک جلوگیری می کنند.", "description": "ژل دندانی با میکروسیلور که پلاک، جرم و التهاب لثه را هدف قرار می دهد. بیوفلاونوئیدها از باکتری های پلاک جلوگیری می کنند.",
"ingredients": "آب، نقره، گلیسیرین، عصاره دانه گریپ فروت.", "ingredients": "آب، نقره، گلیسیرین، عصاره دانه گریپ فروت.",
"usage_and_dosage": "روزانه عصرها 1 ساعت پس از غذا روی دندان های آسیاب مالیده شود. سگ تا 10 کیلو: 1 سانتی متر ژل.", "usage_and_dosage": "روزانه عصرها 1 ساعت پس از غذا روی دندان های آسیاب مالیده شود. سگ تا 10 کیلو: 1 سانتی متر ژل.",
@ -250,6 +277,7 @@
}, },
{ {
"name": "Canina NovaGard Green Augenpflege", "name": "Canina NovaGard Green Augenpflege",
"category_slug": "special-care",
"description": "قطره چشم برای تمیز کردن و مراقبت از چشم های گریان و گرفتگی چشم. نقره کلوئیدی از التهاب جلوگیری می کند.", "description": "قطره چشم برای تمیز کردن و مراقبت از چشم های گریان و گرفتگی چشم. نقره کلوئیدی از التهاب جلوگیری می کند.",
"ingredients": "آب بدون مواد معدنی، هیالورونات سدیم، نقره کلوئیدی.", "ingredients": "آب بدون مواد معدنی، هیالورونات سدیم، نقره کلوئیدی.",
"usage_and_dosage": "چند قطره روی کیسه ملتحمه ریخته و اطراف چشم پاک شود.", "usage_and_dosage": "چند قطره روی کیسه ملتحمه ریخته و اطراف چشم پاک شود.",
@ -259,6 +287,7 @@
}, },
{ {
"name": "Canina NovaGard Green Pfotenpflege", "name": "Canina NovaGard Green Pfotenpflege",
"category_slug": "special-care",
"description": "کرم مراقبت از پنجه 100% طبیعی برای تسکین پوست خارش دار، التیام زخم ها و خراشیدگی پوست سگ ها در زمستان و تابستان.", "description": "کرم مراقبت از پنجه 100% طبیعی برای تسکین پوست خارش دار، التیام زخم ها و خراشیدگی پوست سگ ها در زمستان و تابستان.",
"ingredients": "وازلین، روغن پالپ سنجد دریایی، روغن کالاندولا، پانتنول.", "ingredients": "وازلین، روغن پالپ سنجد دریایی، روغن کالاندولا، پانتنول.",
"usage_and_dosage": "چندین بار در روز روی نواحی آسیب دیده بمالید و ماساژ دهید.", "usage_and_dosage": "چندین بار در روز روی نواحی آسیب دیده بمالید و ماساژ دهید.",

View 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();
});

View File

@ -14,6 +14,7 @@ import Categories from './pages/Categories';
import Blogs from './pages/Blogs'; import Blogs from './pages/Blogs';
import Wiki from './pages/Wiki'; import Wiki from './pages/Wiki';
import Pets from './pages/Pets'; import Pets from './pages/Pets';
import UITexts from './pages/UITexts';
function App() { function App() {
return ( return (
@ -33,7 +34,8 @@ function App() {
<Route path="categories" element={<Categories />} /> <Route path="categories" element={<Categories />} />
<Route path="blogs" element={<Blogs />} /> <Route path="blogs" element={<Blogs />} />
<Route path="wiki" element={<Wiki />} /> <Route path="wiki" element={<Wiki />} />
<Route path="pets" element={<Pets />} /> <Route path="pets" element={<Pets />} />
<Route path="ui-texts" element={<UITexts />} />
</Route> </Route>
</Route> </Route>
</Routes> </Routes>

View File

@ -1,5 +1,5 @@
import { Link, useLocation, useNavigate } from 'react-router-dom'; 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 = [ const menuGroups = [
{ {
@ -36,6 +36,7 @@ const menuGroups = [
title: 'سیستم', title: 'سیستم',
items: [ items: [
{ icon: Settings, label: 'تنظیمات', path: '/settings' }, { icon: Settings, label: 'تنظیمات', path: '/settings' },
{ icon: Languages, label: 'متون رابط کاربری', path: '/ui-texts' },
] ]
} }
]; ];

View 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>
);
}

View File

@ -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"; @import "tailwindcss";
@theme { @theme {
--font-sans: "Vazirmatn", "Inter", sans-serif; --font-sans: "Vazirmatn", sans-serif;
--font-vazir: "Vazirmatn", sans-serif; --font-vazir: "Vazirmatn", sans-serif;
--font-inter: "Inter", sans-serif;
/* Brand Colors */ /* Brand Colors */
--color-canina-blue: #1E4053; --color-canina-blue: #1E4053;
@ -27,11 +33,11 @@
@layer base { @layer base {
:root { :root {
font-family: "Vazirmatn", "Inter", sans-serif !important; font-family: "Vazirmatn", sans-serif !important;
} }
html, body, #root { 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; @apply bg-medical-gray-50 text-medical-gray-900 antialiased;
font-feature-settings: "ss01", "ss02", "cv01", "cv11"; font-feature-settings: "ss01", "ss02", "cv01", "cv11";
} }

View File

@ -40,9 +40,9 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Dynamic products // Dynamic products
try { try {
const products = await productService.getProducts(); const { data: products } = await productService.getProducts();
const productRoutes: MetadataRoute.Sitemap = products.map((product) => ({ const productRoutes: MetadataRoute.Sitemap = products.map((product) => ({
url: `${baseUrl}/shop/${product.id}`, url: `${baseUrl}/shop/${product.slug || product.id}`,
lastModified: new Date(), lastModified: new Date(),
changeFrequency: 'weekly', changeFrequency: 'weekly',
priority: 0.7, priority: 0.7,

View File

@ -28,10 +28,12 @@ import SafeImage from "./SafeImage";
const CATEGORIES = [ const CATEGORIES = [
{ id: "all", label: "همه محصولات", icon: <Activity className="w-4 h-4" /> }, { id: "all", label: "همه محصولات", icon: <Activity className="w-4 h-4" /> },
{ id: "joints", label: "حرکتی و مفاصل", icon: <HeartPulse 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: "immune", label: "ایمنی و گوارش", icon: <ShieldCheck className="w-4 h-4" /> },
{ id: "energy", label: "ویتامین و انرژی", icon: <Activity 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[] }> = { 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 [searchQuery, setSearchQuery] = useState(initialSearch);
const [isUpdating, setIsUpdating] = useState(false); const [isUpdating, setIsUpdating] = useState(false);
const [filteredProducts, setFilteredProducts] = useState<Product[]>([]); 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 // Debug Log for Search Params
useEffect(() => { useEffect(() => {
@ -228,18 +242,22 @@ export default function ArchivePage({
const fetchProducts = async () => { const fetchProducts = async () => {
setIsUpdating(true); setIsUpdating(true);
try { try {
const data = await productService.getProducts({ const res = await productService.getProducts({
category: selectedCategory, category: selectedCategory,
petType: selectedPet, petType: selectedPet,
query: searchQuery query: searchQuery,
page: 1,
limit: 20,
}); });
let result = data; let result = res.data;
if (activeSymptoms.length > 0) { if (activeSymptoms.length > 0) {
result = result.filter(p => p.symptoms.some(s => activeSymptoms.includes(s))); result = result.filter(p => p.symptoms.some(s => activeSymptoms.includes(s)));
} }
setFilteredProducts(result); setFilteredProducts(result);
setMeta(res.meta);
setPage(1);
} catch (error) { } catch (error) {
console.error("Failed to fetch products:", error); console.error("Failed to fetch products:", error);
toast.error("خطا در دریافت لیست محصولات از سرور. لطفاً صفحه را رفرش کنید."); toast.error("خطا در دریافت لیست محصولات از سرور. لطفاً صفحه را رفرش کنید.");
@ -251,6 +269,28 @@ export default function ArchivePage({
fetchProducts(); fetchProducts();
}, [selectedCategory, selectedPet, searchQuery, activeSymptoms]); }, [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) => { const toggleSymptom = (s: string) => {
setActiveSymptoms(prev => prev.includes(s) ? prev.filter(item => item !== s) : [...prev, s]); setActiveSymptoms(prev => prev.includes(s) ? prev.filter(item => item !== s) : [...prev, s]);
}; };
@ -277,11 +317,17 @@ export default function ArchivePage({
</div> </div>
{/* Desktop Breadcrumbs */} {/* 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> <span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/')}>خانه</span>
<ChevronLeft className="w-2.5 h-2.5" /> <ChevronLeft className="w-2.5 h-2.5" />
<span className="text-canina-blue">کاتالوگ کامل محصولات</span> <span className="cursor-pointer hover:text-canina-blue" onClick={() => { setSelectedCategory("all"); }}>کاتالوگ</span>
</div> {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"> <div className="flex flex-col lg:flex-row gap-10">
@ -383,6 +429,7 @@ export default function ArchivePage({
))} ))}
</div> </div>
) : filteredProducts.length > 0 ? ( ) : filteredProducts.length > 0 ? (
<>
<div key="grid" className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6"> <div key="grid" className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
<AnimatePresence mode="popLayout"> <AnimatePresence mode="popLayout">
{filteredProducts.map((p: Product) => ( {filteredProducts.map((p: Product) => (
@ -390,6 +437,17 @@ export default function ArchivePage({
))} ))}
</AnimatePresence> </AnimatePresence>
</div> </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 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"> <div className="w-20 h-20 bg-medical-gray-50 rounded-full flex items-center justify-center text-medical-gray-300 mb-6">

View File

@ -29,7 +29,7 @@ export default function B2BPortal({ onClose }: { onClose: () => void }) {
const [showSuccess, setShowSuccess] = useState(false); const [showSuccess, setShowSuccess] = useState(false);
useEffect(() => { useEffect(() => {
productService.getProducts().then(data => setProducts(data)); productService.getProducts({ limit: 999 }).then(res => setProducts(res.data));
}, []); }, []);
const filteredProducts = products.filter(p => const filteredProducts = products.filter(p =>

View File

@ -24,7 +24,7 @@ export default function BlogPage({ blogs = [] }: { blogs: BlogPost[] }) {
const [products, setProducts] = React.useState<Product[]>([]); const [products, setProducts] = React.useState<Product[]>([]);
React.useEffect(() => { React.useEffect(() => {
productService.getProducts().then(data => setProducts(data)); productService.getProducts({ limit: 10 }).then(res => setProducts(res.data));
}, []); }, []);
if (selectedPost) { if (selectedPost) {
@ -100,7 +100,7 @@ export default function BlogPage({ blogs = [] }: { blogs: BlogPost[] }) {
{products.slice(4, 7).map(p => ( {products.slice(4, 7).map(p => (
<div <div
key={p.id} 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" 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"> <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) => ( {products.slice(0, 2).map((p) => (
<div <div
key={p.id} 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-6 rounded-3xl cursor-pointer hover:bg-white/20 transition-all text-center group" 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" /> <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> <h5 className="text-[10px] font-black">{p.name}</h5>
</div> </div>
))} ))}

View File

@ -38,7 +38,7 @@ export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate
useEffect(() => { useEffect(() => {
if (!hasLachsOl && hasCanhydrox) { 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'); const found = prods.find(p => p.id === 'lachs-ol' || p.artNo === 'lachs-ol');
setSuggestedProduct(found || null); setSuggestedProduct(found || null);
}); });

View File

@ -37,7 +37,7 @@ export default function CheckoutPage() {
const [paymentMethod, setPaymentMethod] = useState<string>('online'); const [paymentMethod, setPaymentMethod] = useState<string>('online');
useEffect(() => { useEffect(() => {
productService.getProducts().then(data => setDbProducts(data)); productService.getProducts({ limit: 999 }).then(res => setDbProducts(res.data));
}, []); }, []);
const subtotal = getSubtotal(); const subtotal = getSubtotal();

View File

@ -8,8 +8,10 @@ import { Eye, ShoppingCart, ShieldCheck, Heart, AlertCircle, Sparkles } from "lu
import SafeImage from "./SafeImage"; import SafeImage from "./SafeImage";
import { ProductCardSkeleton } from "./Skeleton"; import { ProductCardSkeleton } from "./Skeleton";
import Link from 'next/link'; import Link from 'next/link';
import { useSettingsStore } from "../lib/store/settingsStore";
function ProductCard({ product, onClick }: { product: Product; key?: string | number; onClick: (p: Product) => void }) { function ProductCard({ product, onClick }: { product: Product; key?: string | number; onClick: (p: Product) => void }) {
const getText = useSettingsStore(state => state.getText);
const { getActivePet } = usePetStore(); const { getActivePet } = usePetStore();
const activePet = getActivePet(); 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="flex items-center justify-between">
<div className="text-xl font-black text-medical-gray-900 font-vazir">{product.price}</div> <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"> <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> </button>
</div> </div>
</div> </div>
@ -100,6 +102,7 @@ import { toast } from 'sonner';
export default function FeaturedProducts() { export default function FeaturedProducts() {
const router = useRouter(); const router = useRouter();
const getText = useSettingsStore(state => state.getText);
const [products, setProducts] = useState<Product[]>([]); const [products, setProducts] = useState<Product[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -123,21 +126,21 @@ export default function FeaturedProducts() {
<div className="max-w-7xl mx-auto px-4"> <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="flex flex-col lg:flex-row items-end justify-between mb-16 gap-6">
<div className="lg:w-2/3"> <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]"> <h2 className="text-3xl lg:text-5xl font-black text-medical-gray-900 leading-[1.2]">
محصولات برگزیده و راهکارهای <br /> {getText('featured_title_line1', "محصولات برگزیده و راهکارهای")} <br />
<span className="italic text-canina-blue">درمان تخصصی</span> <span className="italic text-canina-blue">{getText('featured_title_line2', "درمان تخصصی")}</span>
</h2> </h2>
</div> </div>
<div className="lg:w-1/3 text-left lg:text-right"> <div className="lg:w-1/3 text-left lg:text-right">
<p className="text-medical-gray-500 text-sm leading-relaxed mb-4"> <p className="text-medical-gray-500 text-sm leading-relaxed mb-4">
محصولات دارویی کانینا با استفاده از دانش پیشرفته بیوتکنولوژی و مواد اولیه ارگانیک نایاب فرآوری شدهاند. {getText('featured_desc', "محصولات دارویی کانینا با استفاده از دانش پیشرفته بیوتکنولوژی و مواد اولیه ارگانیک نایاب فرآوری شده‌اند.")}
</p> </p>
<button <button
onClick={() => router.push('/shop')} 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" 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> <span className="text-lg"></span>
</button> </button>
</div> </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"> <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" /> <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"> <p className="text-sm text-medical-gray-500 font-medium font-vazir leading-relaxed">
در حال حاضر هیچ محصول برجستهای برای نمایش وجود ندارد. لطفاً در پنل ادمین محصولات برگزیده را تنظیم کنید. {getText('featured_no_products_desc', "در حال حاضر هیچ محصول برجسته‌ای برای نمایش وجود ندارد. لطفاً در پنل ادمین محصولات برگزیده را تنظیم کنید.")}
</p> </p>
</div> </div>
)} )}

View File

@ -1,12 +1,14 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { Phone, Mail, MapPin, Instagram, ShieldCheck, Globe } from "lucide-react"; import { Phone, Mail, MapPin, Instagram, ShieldCheck, Globe } from "lucide-react";
import { useSettingsStore } from "../lib/store/settingsStore";
export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: { export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
onNavigate: (v: any) => void, onNavigate: (v: any) => void,
onShopNavigate: (c?: string) => void, onShopNavigate: (c?: string) => void,
onB2BOpen?: () => void onB2BOpen?: () => void
}) { }) {
const getText = useSettingsStore(state => state.getText);
return ( return (
<footer className="bg-medical-gray-900 text-white pt-24 pb-12 font-vazir" dir="rtl"> <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"> <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" /> <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>
<div> <div>
<h3 className="text-2xl font-black italic tracking-tighter">کانینا ایران</h3> <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">نماینده رسمی در ایران</p> <p className="text-[10px] text-white/40 font-bold uppercase tracking-widest">{getText('footer_brand_subtitle', "نماینده رسمی در ایران")}</p>
</div> </div>
</div> </div>
<p className="text-sm text-white/60 leading-relaxed font-medium"> <p className="text-sm text-white/60 leading-relaxed font-medium">
تدارک هوشمندانه سلامت برای همراهان وفادار شما. واردکننده انحصاری مکملهای درمانی با گرید دارویی اختصاصی از آلمان با سابقه ۴۰ سال نوآوری. {getText('footer_brand_desc', "تدارک هوشمندانه سلامت برای همراهان وفادار شما. واردکننده انحصاری مکمل‌های درمانی با گرید دارویی اختصاصی از آلمان با سابقه ۴۰ سال نوآوری.")}
</p> </p>
<div className="flex gap-4"> <div className="flex gap-4">
{[Instagram, Globe].map((Icon, i) => ( {[Instagram, Globe].map((Icon, i) => (
@ -37,34 +39,34 @@ export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
{/* Quick Links */} {/* Quick Links */}
<div className="space-y-8"> <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"> <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"> <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" /> <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>
<li onClick={() => onNavigate("blog")} className="hover:text-white hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group"> <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" /> <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>
<li onClick={() => onNavigate("profile")} className="hover:text-white hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group"> <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" /> <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>
<li onClick={() => onNavigate("wiki")} className="hover:text-white hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group"> <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" /> <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>
<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"> <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" /> <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> </li>
</ul> </ul>
</div> </div>
{/* Location & Contact */} {/* Location & Contact */}
<div className="space-y-8 lg:col-span-2"> <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="grid md:grid-cols-2 gap-8">
<div className="space-y-6"> <div className="space-y-6">
<div className="flex gap-4 group"> <div className="flex gap-4 group">
@ -72,8 +74,8 @@ export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
<Phone className="w-6 h-6" /> <Phone className="w-6 h-6" />
</div> </div>
<div> <div>
<p className="text-[10px] text-white/40 font-black uppercase mb-1">خط ویژه فروش</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">۰۲۱-۸۸۸۸ ۴۴۴۴</p> <p className="text-lg font-black tracking-widest text-left" dir="ltr">{getText('footer_phone', "۰۲۱-۸۸۸۸ ۴۴۴۴")}</p>
</div> </div>
</div> </div>
<div className="flex gap-4 group"> <div className="flex gap-4 group">
@ -81,8 +83,8 @@ export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
<Mail className="w-6 h-6" /> <Mail className="w-6 h-6" />
</div> </div>
<div> <div>
<p className="text-[10px] text-white/40 font-black uppercase mb-1">مکاتبات رسمی</p> <p className="text-[10px] text-white/40 font-black uppercase mb-1">{getText('footer_email_label', "مکاتبات رسمی")}</p>
<p className="text-sm font-bold">info@canino-iran.com</p> <p className="text-sm font-bold">{getText('footer_email', "info@canino-iran.com")}</p>
</div> </div>
</div> </div>
</div> </div>
@ -92,8 +94,8 @@ export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
<MapPin className="w-6 h-6" /> <MapPin className="w-6 h-6" />
</div> </div>
<div> <div>
<p className="text-[10px] text-white/40 font-black uppercase mb-1">دفتر مرکزی</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">تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹</p> <p className="text-sm font-bold leading-relaxed">{getText('footer_address', "تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹")}</p>
</div> </div>
</div> </div>
</div> </div>
@ -104,22 +106,22 @@ export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
<ShieldCheck className="w-6 h-6" /> <ShieldCheck className="w-6 h-6" />
</div> </div>
<div> <div>
<h5 className="text-sm font-black">اصالت کالا تضمین میشود</h5> <h5 className="text-sm font-black">{getText('footer_auth_title', "اصالت کالا تضمین می‌شود")}</h5>
<p className="text-[10px] text-white/40 font-bold">تمامی محصولات دارای لیبل اصالت وزارت بهداشت و QR Code اختصاصی هستند.</p> <p className="text-[10px] text-white/40 font-bold">{getText('footer_auth_desc', "تمامی محصولات دارای لیبل اصالت وزارت بهداشت و QR Code اختصاصی هستند.")}</p>
</div> </div>
</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>
</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 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"> <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">{getText('footer_about', "درباره ما")}</button>
<button onClick={() => onNavigate("home")} className="hover:text-white transition-colors">تماس با ما</button> <button onClick={() => onNavigate("home")} className="hover:text-white transition-colors">{getText('footer_contact', "تماس با ما")}</button>
<button className="hover:text-white transition-colors">حریم خصوصی</button> <button className="hover:text-white transition-colors">{getText('footer_privacy', "حریم خصوصی")}</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,6 +5,7 @@ import { motion, AnimatePresence } from "motion/react";
import { useCartStore } from "../lib/store/cartStore"; import { useCartStore } from "../lib/store/cartStore";
import { usePetStore } from "../lib/store/usePetStore"; import { usePetStore } from "../lib/store/usePetStore";
import { useUserStore } from "../lib/store/userStore"; import { useUserStore } from "../lib/store/userStore";
import { useSettingsStore } from "../lib/store/settingsStore";
import { toast } from "sonner"; import { toast } from "sonner";
import HeaderButton from "./HeaderButton"; import HeaderButton from "./HeaderButton";
import AuthModal from "./AuthModal"; import AuthModal from "./AuthModal";
@ -66,6 +67,7 @@ export default function Header({
const { pets, activePetId, setActivePet, getActivePet } = usePetStore(); const { pets, activePetId, setActivePet, getActivePet } = usePetStore();
const { role, isLoggedIn, logout, profile } = useUserStore(); const { role, isLoggedIn, logout, profile } = useUserStore();
const activePet = getActivePet(); const activePet = getActivePet();
const getText = useSettingsStore(state => state.getText);
// Click Outside logic // Click Outside logic
useEffect(() => { 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"> <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>ارسال رایگان برای سفارشهای بالای ۱۵۰ هزار تومان</span> <span>{getText('shipping_notice', "ارسال رایگان برای سفارش‌های بالای ۱۵۰ هزار تومان")}</span>
</div> </div>
<header className="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-medical-gray-100 shadow-sm"> <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="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"> <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-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">نماینده رسمی Canina Pharma GmbH آلمان</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> </div>
</div> </div>
@ -131,7 +133,7 @@ export default function Header({
onMouseLeave={() => setIsMegaMenuOpen(false)} 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`}> <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' : ''}`} /> <ChevronDown className={`w-4 h-4 transition-transform duration-300 ${isMegaMenuOpen ? 'rotate-180' : ''}`} />
</button> </button>
@ -181,10 +183,10 @@ export default function Header({
</div> </div>
{[ {[
{ id: 'shop', label: 'محصولات تخصصی', action: () => onShopNavigate() }, { id: 'shop', label: getText('nav_products', 'محصولات تخصصی'), action: () => onShopNavigate() },
{ id: 'wiki', label: 'دانشنامه علمی', action: () => onNavigate('wiki') }, { id: 'wiki', label: getText('nav_wiki', 'دانشنامه علمی'), action: () => onNavigate('wiki') },
{ id: 'blog', label: 'مجله سلامت پت', action: () => onNavigate('blog') }, { id: 'blog', label: getText('nav_blog', 'مجله سلامت پت'), action: () => onNavigate('blog') },
{ id: 'profile', label: 'شناسنامه پتها', action: () => onNavigate({ view: 'profile', subview: 'index' }) } { id: 'profile', label: getText('nav_pet_profiles', 'شناسنامه پتها'), action: () => onNavigate({ view: 'profile', subview: 'index' }) }
].map((item) => ( ].map((item) => (
<button <button
key={item.id} key={item.id}
@ -217,11 +219,11 @@ export default function Header({
value={searchQuery} value={searchQuery}
onChange={e => setSearchQuery(e.target.value)} onChange={e => setSearchQuery(e.target.value)}
onBlur={() => !searchQuery && setIsSearchFocused(false)} 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" 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"> <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> </button>
</form> </form>
</motion.div> </motion.div>
@ -282,7 +284,7 @@ export default function Header({
exit={{ opacity: 0, y: 10, scale: 0.95 }} 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" 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"> <div className="space-y-1 mb-4">
{pets.map(pet => ( {pets.map(pet => (
<div <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" 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" /> <PlusCircle className="w-5 h-5" />
ثبت همدم (Pet) جدید {getText('nav_register_pet', "ثبت همدم (Pet) جدید")}
</button> </button>
<button <button
onClick={() => { onNavigate({ view: "user-dashboard" }); setIsPetSwitcherOpen(false); }} 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" 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" /> <Wallet className="w-5 h-5" />
کیف پول و سفارشات {getText('nav_wallet_orders', "کیف پول و سفارشات")}
</button> </button>
<button <button
onClick={() => { logout(); setIsPetSwitcherOpen(false); }} 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" 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" /> <LogOut className="w-5 h-5" />
خروج از حساب کانینا {getText('nav_logout', "خروج از حساب کانینا")}
</button> </button>
</div> </div>
</motion.div> </motion.div>
@ -363,7 +365,7 @@ export default function Header({
onClick={() => setIsAuthModalOpen(true)} onClick={() => setIsAuthModalOpen(true)}
icon={<LogIn className="w-5 h-5" />} icon={<LogIn className="w-5 h-5" />}
> >
ورود / ثبتنام {getText('nav_login', "ورود / ثبت‌نام")}
</HeaderButton> </HeaderButton>
)} )}
@ -374,7 +376,7 @@ export default function Header({
badge={getTotalItems()} badge={getTotalItems()}
icon={<ShoppingBag className="w-6 h-6" />} 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> </HeaderButton>
</div> </div>
</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 justify-between border-b border-medical-gray-100 pb-4">
<div className="flex items-center gap-3"> <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> <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> </div>
<button <button
onClick={() => setIsMobileMenuOpen(false)} onClick={() => setIsMobileMenuOpen(false)}
@ -423,7 +425,7 @@ export default function Header({
onClick={() => setIsMobileSolutionsOpen(!isMobileSolutionsOpen)} 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" 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' : ''}`} /> <ChevronDown className={`w-4 h-4 transition-transform duration-300 ${isMobileSolutionsOpen ? 'rotate-180' : ''}`} />
</button> </button>
<AnimatePresence> <AnimatePresence>
@ -469,10 +471,10 @@ export default function Header({
</div> </div>
{[ {[
{ id: 'shop', label: 'محصولات تخصصی', action: () => onShopNavigate() }, { id: 'shop', label: getText('nav_products', 'محصولات تخصصی'), action: () => onShopNavigate() },
{ id: 'wiki', label: 'دانشنامه علمی', action: () => onNavigate('wiki') }, { id: 'wiki', label: getText('nav_wiki', 'دانشنامه علمی'), action: () => onNavigate('wiki') },
{ id: 'blog', label: 'مجله سلامت پت', action: () => onNavigate('blog') }, { id: 'blog', label: getText('nav_blog', 'مجله سلامت پت'), action: () => onNavigate('blog') },
{ id: 'profile', label: 'شناسنامه پتها', action: () => onNavigate({ view: 'profile', subview: 'index' }) } { id: 'profile', label: getText('nav_pet_profiles', 'شناسنامه پتها'), action: () => onNavigate({ view: 'profile', subview: 'index' }) }
].map((item) => ( ].map((item) => (
<button <button
key={item.id} key={item.id}

View File

@ -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"> <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 <img
src={banners.length > 0 && banners[0].imageUrl ? banners[0].imageUrl : "https://images.unsplash.com/photo-1583337130417-3346a1be7dee?w=800&q=80"} src={banners.length > 0 && banners[0].imageUrl ? banners[0].imageUrl : "/assets/images/hero-section-image.png"}
alt={banners.length > 0 ? banners[0].title : "German Veterinary Expert"} alt={banners.length > 0 ? banners[0].title : "Canina Pharma Germany"}
className="w-full h-full object-cover grayscale-[20%] hover:grayscale-0 transition-all duration-700" className="w-full h-full object-cover"
loading="eager" loading="eager"
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
/> />

View File

@ -2,7 +2,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Product, IngredientInfo } from "../lib/data/products"; import { Product, IngredientInfo } from "../lib/data/products";
import { motion } from "motion/react"; 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 { useSettingsStore } from "../lib/store/settingsStore";
import { productService } from "../lib/services/productService"; import { productService } from "../lib/services/productService";
@ -13,9 +13,10 @@ export default function IngredientWiki() {
const texts = useSettingsStore(state => state.texts); const texts = useSettingsStore(state => state.texts);
const [ingredientsWiki, setIngredientsWiki] = useState<IngredientInfo[]>([]); const [ingredientsWiki, setIngredientsWiki] = useState<IngredientInfo[]>([]);
const [products, setProducts] = useState<Product[]>([]); const [products, setProducts] = useState<Product[]>([]);
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => { useEffect(() => {
productService.getProducts().then(data => setProducts(data)); productService.getProducts({ limit: 999 }).then(res => setProducts(res.data));
}, []); }, []);
useEffect(() => { 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 className="mt-6 text-lg text-medical-gray-500 max-w-2xl leading-relaxed">
ما از مواد اولیهای استفاده میکنیم که در سطح دارویی فرآوری شدهاند. در اینجا با قدرت بیولوژیک این ترکیبات آشنا شوید. ما از مواد اولیهای استفاده میکنیم که در سطح دارویی فرآوری شدهاند. در اینجا با قدرت بیولوژیک این ترکیبات آشنا شوید.
</p> </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> </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"> <div className="space-y-32">
{ingredientsWiki.map((ing, idx) => { {filtered.map((ing, idx) => {
const hasIngredient = products.filter(p => 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.name.toLowerCase())) ||
p.main_ingredients?.some(mi => mi.toLowerCase().includes(ing.id.replace('-', ' '))) 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 => ( {hasIngredient.length > 0 ? hasIngredient.map(p => (
<div <div
key={p.id} 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" 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" /> <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> <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>
)) : ( )) : (
<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>
)} )}
</div> </div>
@ -154,6 +187,8 @@ export default function IngredientWiki() {
); );
})} })}
</div> </div>
);
})()}
{/* Quality Badge */} {/* Quality Badge */}
<div className="mt-32 p-12 bg-medical-gray-900 rounded-[4rem] text-center text-white relative overflow-hidden"> <div className="mt-32 p-12 bg-medical-gray-900 rounded-[4rem] text-center text-white relative overflow-hidden">

View File

@ -49,8 +49,8 @@ export default function PetProfile({ initialView, advisorNeed }: {
const [products, setProducts] = useState<Product[]>([]); const [products, setProducts] = useState<Product[]>([]);
useEffect(() => { useEffect(() => {
productService.getProducts() productService.getProducts({ limit: 999 })
.then(setProducts) .then(res => setProducts(res.data))
.catch(err => console.error("Error fetching products in PetProfile:", err)); .catch(err => console.error("Error fetching products in PetProfile:", err));
}, []); }, []);

View File

@ -79,8 +79,9 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
const [allProducts, setAllProducts] = useState<Product[]>([]); const [allProducts, setAllProducts] = useState<Product[]>([]);
useEffect(() => { useEffect(() => {
productService.getProducts() productService.getProducts({ limit: 999 })
.then((products) => { .then((res) => {
const products = res.data;
setAllProducts(products); setAllProducts(products);
const found = products.find(p => p.slug === productSlug || p.id === productSlug); const found = products.find(p => p.slug === productSlug || p.id === productSlug);
setProduct(found || null); setProduct(found || null);
@ -225,7 +226,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
</div> </div>
{/* Key Benefits Section */} {/* Key Benefits Section */}
{fullProduct.keyBenefits?.length > 0 && ( {fullProduct.keyBenefits && fullProduct.keyBenefits.length > 0 && (
<section className="space-y-8"> <section className="space-y-8">
<div className="flex items-center gap-4"> <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"> <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> </section>
{/* FAQ Section */} {/* FAQ Section */}
{product.faqs?.length > 0 && ( {product.faqs && product.faqs.length > 0 && (
<section className="pt-10"> <section className="pt-10">
<div className="flex items-center gap-4 mb-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"> <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> </div>
{/* expected results box */} {/* 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"> <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> <h4 className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-6 font-vazir">تغییرات قابل انتظار</h4>
<div className="space-y-4"> <div className="space-y-4">

View File

@ -15,8 +15,8 @@ export default function SearchResultsPage({ query }: { query: string }) {
const [products, setProducts] = useState<Product[]>([]); const [products, setProducts] = useState<Product[]>([]);
useEffect(() => { useEffect(() => {
productService.getProducts() productService.getProducts({ limit: 999 })
.then(setProducts) .then(res => setProducts(res.data))
.catch(err => console.error("Error fetching products in SearchResultsPage:", err)); .catch(err => console.error("Error fetching products in SearchResultsPage:", err));
}, []); }, []);

View File

@ -40,6 +40,7 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
const setLoginModalOpen = useUIStore(state => state.setLoginModalOpen); const setLoginModalOpen = useUIStore(state => state.setLoginModalOpen);
const { addPet, setActivePet } = usePetStore(); const { addPet, setActivePet } = usePetStore();
const texts = useSettingsStore(state => state.texts); const texts = useSettingsStore(state => state.texts);
const getText = useSettingsStore(state => state.getText);
const medicalOptions = React.useMemo(() => { const medicalOptions = React.useMemo(() => {
const jsonStr = texts['medical_options']; const jsonStr = texts['medical_options'];
if (jsonStr) { 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" 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" /> <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> </motion.div>
<h2 className="text-4xl lg:text-5xl font-black text-medical-gray-900 mb-4 font-vazir italic">دستیار سلامت کانینا</h2> <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">فقط چند قدم تا صدور شناسنامه هوشمند و دریافت پیشنهادات تخصصی</p> <p className="text-medical-gray-600 font-bold text-lg font-vazir">{getText('advisor_subtitle', "فقط چند قدم تا صدور شناسنامه هوشمند و دریافت پیشنهادات تخصصی")}</p>
</div> </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"> <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" className="space-y-8"
> >
<div className="text-center"> <div className="text-center">
<h3 className="text-2xl font-black text-medical-gray-900 mb-2 font-vazir uppercase italic tracking-tight">گام اول: هویت بصری</h3> <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">همدم شما رو با چه اسمی صدا میزنید؟</p> <p className="text-medical-gray-600 font-bold font-vazir">{getText('advisor_step1_desc', "همدم شما رو با چه اسمی صدا می‌زنید؟")}</p>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <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="grid md:grid-cols-2 gap-6">
<div className="space-y-2"> <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 <input
type="text" type="text"
value={name} value={name}
onChange={e => setName(e.target.value)} 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" 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>
<div className="space-y-2"> <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 <input
type="text" type="text"
value={breed} value={breed}
onChange={e => setBreed(e.target.value)} 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" 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> </div>
@ -174,12 +175,12 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
disabled={!name || !breed} 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" 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" /> <ChevronLeft className="w-5 h-5" />
</button> </button>
{(!name || !breed) && ( {(!name || !breed) && (
<p className="text-center text-xs font-black text-amber-600 mt-2 font-vazir animate-pulse"> <p className="text-center text-xs font-black text-amber-600 mt-2 font-vazir animate-pulse">
لطفاً ابتدا نام و نژاد پت را در بالا وارد کنید. {getText('advisor_validation_warning', "⚠️ لطفاً ابتدا نام و نژاد پت را در بالا وارد کنید.")}
</p> </p>
)} )}
</motion.div> </motion.div>
@ -194,13 +195,13 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
className="space-y-8" className="space-y-8"
> >
<div className="text-center"> <div className="text-center">
<h3 className="text-2xl font-black text-medical-gray-900 mb-2 font-vazir uppercase italic tracking-tight">گام دوم: پایش فیزیکی</h3> <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">اطلاعات فیزیکی دقیق به دوزبندی صحیح مکملها کمک میکند</p> <p className="text-medical-gray-600 font-bold font-vazir">{getText('advisor_step2_desc', "اطلاعات فیزیکی دقیق به دوزبندی صحیح مکمل‌ها کمک می‌کند")}</p>
</div> </div>
<div className="grid grid-cols-2 gap-6"> <div className="grid grid-cols-2 gap-6">
<div className="space-y-4"> <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"> <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> <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> <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> </div>
<div className="space-y-4"> <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"> <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> <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> <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>
<div className="space-y-4"> <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"> <div className="grid grid-cols-3 gap-3">
{["کم", "متوسط", "زیاد"].map(level => ( {["کم", "متوسط", "زیاد"].map(level => (
<button <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]" 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" /> <ChevronRight className="w-5 h-5" />
قبلی {getText('advisor_back', "قبلی")}
</button> </button>
<button <button
onClick={handleNext} 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]" 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" /> <ChevronLeft className="w-5 h-5" />
</button> </button>
</div> </div>
@ -260,8 +261,8 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
className="space-y-8" className="space-y-8"
> >
<div className="text-center"> <div className="text-center">
<h3 className="text-2xl font-black text-medical-gray-900 mb-2 font-vazir uppercase italic tracking-tight">گام آخر: سوابق و حساسیتها</h3> <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">در صورت وجود هر یک از موارد زیر، آن را تیک بزنید</p> <p className="text-medical-gray-600 font-bold font-vazir">{getText('advisor_step3_desc', "در صورت وجود هر یک از موارد زیر، آن را تیک بزنید")}</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <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]" 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" /> <ChevronRight className="w-5 h-5" />
قبلی {getText('advisor_back', "قبلی")}
</button> </button>
<button <button
onClick={handleSubmit} 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]" 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" /> <ChevronLeft className="w-5 h-5" />
</button> </button>
</div> </div>

View File

@ -3,6 +3,7 @@ import React, { useState } from "react";
import { Play, PlayCircle, Star, ShieldCheck, X } from "lucide-react"; import { Play, PlayCircle, Star, ShieldCheck, X } from "lucide-react";
import { motion, AnimatePresence } from "motion/react"; import { motion, AnimatePresence } from "motion/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useSettingsStore } from "../lib/store/settingsStore";
const VIDEOS = [ const VIDEOS = [
{ {
@ -36,6 +37,7 @@ const VIDEOS = [
export default function VetGallery({ testimonials = [] }: { testimonials?: any[] }) { export default function VetGallery({ testimonials = [] }: { testimonials?: any[] }) {
const router = useRouter(); const router = useRouter();
const getText = useSettingsStore(state => state.getText);
const displayItems = testimonials.length > 0 ? testimonials : VIDEOS; const displayItems = testimonials.length > 0 ? testimonials : VIDEOS;
const [selectedVideo, setSelectedVideo] = useState<any>(null); const [selectedVideo, setSelectedVideo] = useState<any>(null);
@ -46,20 +48,20 @@ export default function VetGallery({ testimonials = [] }: { testimonials?: any[]
<div className="max-w-2xl"> <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"> <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" /> <Star className="w-3 h-3" />
Vet Insights {getText('videos_badge', "Vet Insights")}
</div> </div>
<h2 className="text-4xl md:text-5xl font-black text-medical-gray-900 italic leading-tight"> <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> </h2>
<p className="text-lg text-medical-gray-500 font-medium mt-6"> <p className="text-lg text-medical-gray-500 font-medium mt-6">
متخصصین آلمانی کانینا مستقیماً با شما صحبت میکنند. نحوه مصرف، مزایای رقابتی و دانش تخصصی پشت هر محصول. {getText('videos_desc', "متخصصین آلمانی کانینا مستقیماً با شما صحبت می‌کنند. نحوه مصرف، مزایای رقابتی و دانش تخصصی پشت هر محصول.")}
</p> </p>
</div> </div>
<button <button
onClick={() => router.push('/videos')} 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" 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" /> <Play className="w-4 h-4" />
</button> </button>
</div> </div>

View File

@ -77,18 +77,25 @@ export class ProductService {
category?: string; category?: string;
petType?: PetType | "all"; petType?: PetType | "all";
query?: string; query?: string;
}): Promise<Product[]> { page?: number;
limit?: number;
}): Promise<{ data: Product[]; meta: { total: number; page: number; lastPage: number; limit: number } }> {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (filters?.category && filters.category !== "all") params.append('category', filters.category); if (filters?.category && filters.category !== "all") params.append('category', filters.category);
if (filters?.petType && filters.petType !== "all") params.append('petType', filters.petType); if (filters?.petType && filters.petType !== "all") params.append('petType', filters.petType);
if (filters?.query) params.append('search', filters.query); if (filters?.query) params.append('search', filters.query);
params.append('page', String(filters?.page || 1));
params.append('limit', String(filters?.limit || 12));
try { try {
const response = await api.get(`/products?${params.toString()}`); 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) { } catch (error) {
console.error("[ProductService] Failed to fetch products:", error); console.error("[ProductService] Failed to fetch products:", error);
return []; return { data: [], meta: { total: 0, page: 1, lastPage: 0, limit: 12 } };
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 MiB