feat: nextjs setup and configuration (lib, store, css, assets)
This commit is contained in:
parent
eff6c00a9c
commit
c5a1b8e5d2
@ -1,26 +1,74 @@
|
||||
@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');
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
@theme {
|
||||
--font-sans: "Vazirmatn", "Inter", sans-serif;
|
||||
--font-vazir: "Vazirmatn", sans-serif;
|
||||
--font-inter: "Inter", sans-serif;
|
||||
|
||||
--color-canina-blue: #004182;
|
||||
--color-medical-gray-50: #f8fafc;
|
||||
--color-medical-gray-100: #f1f5f9;
|
||||
--color-medical-gray-200: #e2e8f0;
|
||||
--color-medical-gray-300: #cbd5e1;
|
||||
--color-medical-gray-400: #94a3b8;
|
||||
--color-medical-gray-500: #64748b;
|
||||
--color-medical-gray-600: #475569;
|
||||
--color-medical-gray-700: #334155;
|
||||
--color-medical-gray-800: #1e293b;
|
||||
--color-medical-gray-900: #0f172a;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@layer base {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
font-family: "Vazirmatn", "Inter", sans-serif !important;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
font-family: "Vazirmatn", "Inter", sans-serif !important;
|
||||
@apply bg-medical-gray-50 text-medical-gray-900 antialiased;
|
||||
font-feature-settings: "ss01", "ss02", "cv01", "cv11";
|
||||
}
|
||||
|
||||
button, a, [role="button"], .cursor-pointer {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
/* Smooth hover transitions for all interactive elements */
|
||||
button, a, .group, .transition-all {
|
||||
@apply transition-all duration-300;
|
||||
}
|
||||
|
||||
/* Default mild hover effects */
|
||||
button:hover, .cursor-pointer:hover {
|
||||
@apply opacity-90;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
@apply opacity-80;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
@layer utilities {
|
||||
.font-vazir {
|
||||
font-family: "Vazirmatn", sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar for a professional look */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-medical-gray-100);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-medical-gray-300);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-canina-blue);
|
||||
}
|
||||
|
||||
513
frontend/application/lib/data/products.ts
Normal file
513
frontend/application/lib/data/products.ts
Normal file
@ -0,0 +1,513 @@
|
||||
export interface Specialist {
|
||||
name: string;
|
||||
title: string;
|
||||
image: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type PetType = "سگ" | "گربه" | "هر دو";
|
||||
|
||||
export interface IngredientInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
benefits: string[];
|
||||
}
|
||||
|
||||
export interface DosageResult {
|
||||
quantity: number;
|
||||
unit: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface FAQ {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
artNo: string;
|
||||
name: string;
|
||||
scientificTagline: string;
|
||||
description: string;
|
||||
shortDescription?: string;
|
||||
keyBenefits?: { icon: string; title: string; description: string }[];
|
||||
price: string;
|
||||
priceValue: number;
|
||||
category: string;
|
||||
categorySlug?: string;
|
||||
unit: string;
|
||||
packageSize: number;
|
||||
main_ingredients: string[];
|
||||
dosage_logic: string;
|
||||
benefits: string;
|
||||
benefitsList?: string[];
|
||||
storage?: string;
|
||||
symptoms: string[];
|
||||
suitableFor: PetType;
|
||||
specialBadge?: string;
|
||||
contraindications?: string[];
|
||||
onSetOfAction?: string;
|
||||
expectedResults?: { icon: string; text: string }[];
|
||||
optimisticTemplate?: string;
|
||||
calculateDosage: (weight: number, isYoung: boolean) => DosageResult;
|
||||
analysis: Record<string, string>;
|
||||
feedingAdvice: string;
|
||||
specialist: Specialist;
|
||||
image: string;
|
||||
relatedProducts?: string[];
|
||||
faqs?: FAQ[];
|
||||
}
|
||||
|
||||
export const INGREDIENTS_WIKI: IngredientInfo[] = [
|
||||
{
|
||||
id: "green-mussel",
|
||||
name: "صدف لبسبز (Perna Canaliculus)",
|
||||
description: "این صدف بومی سواحل بکر نیوزیلند است و تنها منبع طبیعی حاوی گلیکوزآمینگلیکانها (GAG) در غلظت بسیار بالا محسوب میشود.",
|
||||
benefits: [
|
||||
"بازسازی بافت غضروفی تخریب شده",
|
||||
"خواص ضدالتهابی طبیعی برای رباطها",
|
||||
"تامین اسیدهای چرب امگا ۳ خاص",
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "silver",
|
||||
name: "نقره میکروسیلور (Microsilver)",
|
||||
description: "ذرات نقره خالص با ساختار اسفنجی که به دلیل اندازه ذرات درشت، جذب خون نمیشوند اما اثر آنتیباکتریال پایداری بر سطح غشاها دارند.",
|
||||
benefits: [
|
||||
"مبارزه با باکتریهای مقاوم در دهان و پوست",
|
||||
"عدم آسیب به فلور طبیعی بدن",
|
||||
"جایگزین ایمن برای آنتیبیوتیکهای موضعی",
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "colostrum",
|
||||
name: "آغوز (Colostrum)",
|
||||
description: "اولین شیر مادر که حاوی دوز بالایی از ایمونوگلوبولینها، ویتامینها و مواد معدنی برای فعالسازی فوری سیستم ایمنی است.",
|
||||
benefits: [
|
||||
"تقویت سد دفاعی روده",
|
||||
"انتقال مستقیم آنتیبادی به نوزادان",
|
||||
"تسریع نقاهت بعد از جرافی",
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "peat-extract",
|
||||
name: "عصاره پیت (Peat Extract)",
|
||||
description: "مادهای کاملا طبیعی حاصل از تجزیه گیاهان در طول هزاران سال که غنی از اسیدهای هومیک و مواد معدنی است.",
|
||||
benefits: [
|
||||
"جذب سموم در دستگاه گوارش",
|
||||
"تنظیم فلور روده",
|
||||
"بهبود اشتها و هضم"
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export const PRODUCTS: Product[] = [
|
||||
{
|
||||
id: "canhydrox-gag",
|
||||
artNo: "123101",
|
||||
name: "Canhydrox GAG (کنهیدروکس)",
|
||||
scientificTagline: "استاندارد طلایی ارتوپدی سگهای آلمان",
|
||||
description: "درمان تخصصی مفاصل، استخوان، تاندون و دندان در سگهای بزرگ و سنگینوزن.",
|
||||
shortDescription: "قویترین فرمول ارتوپدی آلمان برای تقویت اسکلتبندی و رشد سالم استخوانها در نژادهای بزرگ.",
|
||||
keyBenefits: [
|
||||
{ icon: "ShieldCheck", title: "هیدروکسی آپاتیت", description: "جذب مستقیم در استخوان" },
|
||||
{ icon: "Activity", title: "صدف لبسبز", description: "بازسازی کامل غضروفها" },
|
||||
{ icon: "Flame", title: "گرید دارویی", description: "بالاترین سطح خلوص مواد" }
|
||||
],
|
||||
category: "مفاصل و استخوان",
|
||||
categorySlug: "joints",
|
||||
price: "۴,۲۴۰,۰۰۰ تومان",
|
||||
priceValue: 4240000,
|
||||
unit: "60 Tablets / 100g",
|
||||
packageSize: 60,
|
||||
main_ingredients: ["30% تریکلسیم فسفات", "10% پودر صدف لبسبز", "ویتامین C", "متیونین", "سلنیوم", "هیدروکسی آپاتیت"],
|
||||
dosage_logic: "جوان (از 8 هفتگی): 2 قرص به ازای 10kg وزن (تا 15 ماهگی). مسن/کاری: 4 قرص به ازای 10kg وزن. حداکثر 16 قرص.",
|
||||
benefits: "درمان تخصصی مفاصل، استخوان، تاندون و دندان در سگهای بزرگ و سنگینوزن.",
|
||||
benefitsList: [
|
||||
"تقویت بافت همبند، مفاصل و غضروفها",
|
||||
"پیشگیری از مشکلات مفاصل در نژادهای بزرگ",
|
||||
"تثبیت استخوانبندی و رشد سالم در تولهها",
|
||||
"بهبود بازسازی استخوان پس از جراحی یا شکستگی"
|
||||
],
|
||||
storage: "در دمای اتاق و دور از تابش مستقیم نور خورشید نگهداری شود.",
|
||||
symptoms: ["لنگیدن", "سختی در بلند شدن", "درد مفاصل", "رشد سریع تولهسگ"],
|
||||
suitableFor: "سگ",
|
||||
specialBadge: "انتخاب اول ارتوپدی",
|
||||
onSetOfAction: "هفته چهارم",
|
||||
optimisticTemplate: "[PetName] از [OnSet] با نشاط بیشتری میدود و بازی میکند.",
|
||||
expectedResults: [
|
||||
{ icon: "Sparkles", text: "استحکام مفاصل" },
|
||||
{ icon: "Activity", text: "حرکت بدون درد" },
|
||||
{ icon: "ShieldCheck", text: "دندانهای قوی" }
|
||||
],
|
||||
calculateDosage: (weight, isYoung) => {
|
||||
const rate = isYoung ? 2 : 4;
|
||||
let qty = (weight / 10) * rate;
|
||||
qty = Math.min(qty, 16);
|
||||
return {
|
||||
quantity: Number(qty.toFixed(1)),
|
||||
unit: "قرص",
|
||||
description: isYoung ? "دوز رشد (تا ۱۵ ماهگی)" : "دوز حمایتی / سگ کاری"
|
||||
};
|
||||
},
|
||||
analysis: { "کلسیم": "۱۸.۲٪", "فسفر": "۷.۱٪", "پروتئین": "۱۲.۴٪" },
|
||||
feedingAdvice: "قرصها را میتوان مستقیماً در دهان قرار داد یا با غذای روزانه مخلوط کرد.",
|
||||
specialist: {
|
||||
name: "دکتر کلاوس هنینگ",
|
||||
title: "متخصص جراحی استخوان و مفاصل دامپزشکی",
|
||||
image: "https://images.unsplash.com/photo-1612349317150-e413f6a5b16d?auto=format&fit=crop&q=80&w=200",
|
||||
message: "کنهیدروکس قویترین محصول بازسازی کننده بافتهای نگهدارنده است. هیدروکسی آپاتیت موجود در آن مستقیماً در توده استخوانی نفوذ کرده و استحکام اسکلت را تضمین میکند."
|
||||
},
|
||||
image: "https://www.canina.de/media/83/86/e1/123000_123005_Canhydrox_GAG_Canina-Pharma_1280x1280.png",
|
||||
relatedProducts: ["lachs-ol"],
|
||||
faqs: [
|
||||
{ question: "آیا برای سگهای در حال رشد مناسب است؟", answer: "بله، این محصول به طور ویژه برای سگهای نژاد بزرگ از هفته ۸ تا ماه ۱۵ برای استحکام اسکلت و دندانها توصیه میشود." },
|
||||
{ question: "آیا برای دندانها هم موثر است؟", answer: "بله، بیش از ۹۰٪ بافت دندان از هیدروکسی آپاتیت ساخته شده که ماده اصلی این محصول است." }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "immun-booster",
|
||||
artNo: "140504",
|
||||
name: "Immun-Booster (ایمیون بوستر)",
|
||||
scientificTagline: "سیستم دفاعی هوشمند بیولوژیک",
|
||||
description: "کمک به تقویت فوری سیستم ایمنی با آغوز و ویتامینهای ضروری.",
|
||||
shortDescription: "سپر دفاعی هوشمند؛ خمیر آغوز متمرکز برای فعالسازی فوری گلبولهای سفید و تقویت تولهها و مادران باردار.",
|
||||
keyBenefits: [
|
||||
{ icon: "ShieldCheck", title: "آغوز باکیفیت", description: "تامین فوری ایمونوگلوبولین" },
|
||||
{ icon: "Zap", title: "جذب سریع", description: "فعالسازی سیستم ایمنی در ۳ روز" },
|
||||
{ icon: "Heart", title: "حمایت حیاتی", description: "مناسب برای دوران نقاهت سنگین" }
|
||||
],
|
||||
category: "تقویت سیستم ایمنی و گوارش",
|
||||
categorySlug: "immune",
|
||||
price: "۲,۹۹۰,۰۰۰ تومان",
|
||||
priceValue: 2990000,
|
||||
unit: "15 ml Paste",
|
||||
packageSize: 15,
|
||||
main_ingredients: ["پودر آغوز (Colostrum)", "روغنهای گیاهی", "ویتامین A, D3, E"],
|
||||
dosage_logic: "تولهسگ: 0.25ml. سگ کوچک (<5kg): 0.25-0.5ml. سگ متوسط (5-10kg): 0.5-1ml. سگ بزرگ (>10kg): 1-2ml.",
|
||||
benefits: "تقویت فوری سیستم ایمنی، حاوی ایمونوگلوبولینهای آغوز، مناسب برای دوران نقاهت و زایمان.",
|
||||
symptoms: ["ضعف بعد از بیماری", "بیاشتهایی", "بعد از زایمان"],
|
||||
suitableFor: "هر دو",
|
||||
specialBadge: "مراقب ویژه بعد از زایمان",
|
||||
onSetOfAction: "روز سوم",
|
||||
optimisticTemplate: "[PetName] تنها پس از [OnSet] انرژی از دست رفتهاش را دوباره به دست میآورد.",
|
||||
expectedResults: [
|
||||
{ icon: "Heart", text: "تقویت ایمنی" },
|
||||
{ icon: "Flame", text: "رفع خستگی" },
|
||||
{ icon: "Zap", text: "هوشیاری بیشتر" }
|
||||
],
|
||||
calculateDosage: (weight, isYoung) => {
|
||||
let ml = 0.25;
|
||||
if (isYoung) ml = 0.25;
|
||||
else if (weight < 5) ml = 0.5;
|
||||
else if (weight <= 10) ml = 1;
|
||||
else ml = 2;
|
||||
return { quantity: ml, unit: "میلیلیتر", description: "خمیر را روی زبان قرار دهید" };
|
||||
},
|
||||
analysis: { "چربی": "۶۵٪", "پروتئین": "۱۵٪" },
|
||||
feedingAdvice: "توصیه میشود در زمانهای استرس (واکسیناسیون، جابجایی) استفاده شود.",
|
||||
specialist: {
|
||||
name: "دکتر النا اشمیت",
|
||||
title: "جراح و متخصص طب داخلی پتها",
|
||||
image: "https://images.unsplash.com/photo-1594824812397-237f41c63230?auto=format&fit=crop&q=80&w=200",
|
||||
message: "خمیر ایمون بوستر با استفاده از آغوز با کیفیت دارویی، بلافاصله سیستم ایمنی غیرفعال پت را فعال میکند."
|
||||
},
|
||||
image: "https://www.canina.de/media/8c/fb/8e/311000_Canina_Immun_Booster_Paste_Abwehrkraefte_1280x1280.png"
|
||||
},
|
||||
{
|
||||
id: "mikrosilber-zahngel",
|
||||
artNo: "131454",
|
||||
name: "Mikrosilber Zahngel (ژل دندان میکروسیلور)",
|
||||
scientificTagline: "مراقبت آنتیسپتیک دهان با تکنولوژی نقره میکروسیمی",
|
||||
description: "کمک به رفع جرم و پلاک دندان با استفاده از ذرات نقره.",
|
||||
shortDescription: "انقلاب در بهداشت دهان؛ ژل نقره میکروسیمی برای حذف باکتریهای مولد جرم و بوی بد دهان بدون نیاز به بیهوشی.",
|
||||
keyBenefits: [
|
||||
{ icon: "Sparkles", title: "نقره میکروسیلور", description: "اثر آنتیباکتریال ۲۴ ساعته" },
|
||||
{ icon: "ShieldCheck", title: "حذف پلاک", description: "جلوگیری از تشکیل رسوبات جدید" },
|
||||
{ icon: "Activity", title: "لثه سالم", description: "کاهش التهاب و قرمزی لثه" }
|
||||
],
|
||||
category: "مراقبتهای ویژه (پوست، دندان و چشم)",
|
||||
categorySlug: "special-care",
|
||||
price: "۲,۷۶۵,۰۰۰ تومان",
|
||||
priceValue: 2765000,
|
||||
unit: "50 ml",
|
||||
packageSize: 50,
|
||||
main_ingredients: ["نقره میکروسیلور", "عصاره دانه گریپفروت", "ویتامین C"],
|
||||
dosage_logic: "تا 10kg: یک سانت ژل. 10-20kg: دو سانت ژل. 20-30kg: سه سانت ژل. بالای 30kg: چهار سانت ژل.",
|
||||
benefits: "رفع پلاک، جرم و التهاب لثه با استفاده از تکنولوژی نقره کلوئیدی.",
|
||||
symptoms: ["جرم دندان", "التهاب لثه", "بوی بد دهان"],
|
||||
suitableFor: "هر دو",
|
||||
onSetOfAction: "هفته دوم",
|
||||
optimisticTemplate: "لبخند سفیدتر و نفس تازهتر [PetName] از [OnSet] نمایان میشود.",
|
||||
expectedResults: [
|
||||
{ icon: "Sparkles", text: "رفع بوی دهان" },
|
||||
{ icon: "ShieldCheck", text: "کاهش جرم" },
|
||||
{ icon: "Heart", text: "لثههای سالم" }
|
||||
],
|
||||
calculateDosage: (weight) => {
|
||||
let cm = 1;
|
||||
if (weight <= 10) cm = 1;
|
||||
else if (weight <= 20) cm = 2;
|
||||
else if (weight <= 30) cm = 3;
|
||||
else cm = 4;
|
||||
return { quantity: cm, unit: "سانتیمتر", description: "روی لثهها ماساژ دهید" };
|
||||
},
|
||||
analysis: { "نقره": "بسیار بالا" },
|
||||
feedingAdvice: "تا ۳۰ دقیقه بعد از استفاده، پت نباید آب یا غذا بخورد.",
|
||||
specialist: {
|
||||
name: "دکتر توماس بروات",
|
||||
title: "متخصص دندانپزشکی دامپزشکی",
|
||||
image: "https://images.unsplash.com/photo-1622253692010-333f2da6031d?auto=format&fit=crop&q=80&w=200",
|
||||
message: "ذرات میکروسیلور در این ژل به قدری درشت هستند که جذب جریان خون نمیشوند اما اثر آنتیباکتریال فوقالعادهای بر سطح لثه دارند."
|
||||
},
|
||||
image: "https://www.canina.de/media/9d/5e/54/131454_Mikrosilber_Zahngel_Canina-Pharma_1280x1280.png"
|
||||
},
|
||||
{
|
||||
id: "ballaststoff-mix",
|
||||
artNo: "140108",
|
||||
name: "Ballaststoff Mix (فیبر میکس)",
|
||||
scientificTagline: "بهینهسازی بیوم روده با فیبرهای گیاهی ارگانیک",
|
||||
description: "ترکیب متعادل فیبرهای گیاهی برای تنظیم سیستم گوارش و بهبود کیفیت دفع.",
|
||||
shortDescription: "راهکار نهایی مشکلات گوارشی؛ تنظیمکننده حرکات روده و کمک به تخلیه طبیعی غدد مقعدی با فیبر سیب.",
|
||||
category: "تقویت سیستم ایمنی و گوارش",
|
||||
categorySlug: "immune",
|
||||
price: "۱,۵۵۰,۰۰۰ تومان",
|
||||
priceValue: 1550000,
|
||||
unit: "100 g",
|
||||
packageSize: 100,
|
||||
main_ingredients: ["فیبر سیب (پکتین)", "یونجه", "نخود فرنگی", "هویج"],
|
||||
dosage_logic: "۰.۵ گرم به ازای هر کیلوگرم وزن بدن. حداکثر ۱۵ گرم در روز.",
|
||||
benefits: "تنظیم حرکات روده، بهبود تخلیه غدد مقعدی، کنترل وزن و ایجاد فلور پایدار در روده.",
|
||||
symptoms: ["یبوست", "اسهال", "تورم غدد مقعدی", "اضافه وزن"],
|
||||
suitableFor: "هر دو",
|
||||
onSetOfAction: "روز چهارم",
|
||||
optimisticTemplate: "سیستم گوارشی [PetName] از حدود [OnSet] شروع به تنظیم مجدد و دفع راحتتر میکند.",
|
||||
expectedResults: [
|
||||
{ icon: "ShieldCheck", text: "دفع منظم" },
|
||||
{ icon: "Activity", text: "کاهش التهاب روده" },
|
||||
{ icon: "Sparkles", text: "کنترل بوی مدفوع" }
|
||||
],
|
||||
calculateDosage: (weight) => {
|
||||
let g = weight * 0.5;
|
||||
g = Math.min(g, 15);
|
||||
return { quantity: Number(g.toFixed(1)), unit: "گرم", description: "با غذا مخلوط شود" };
|
||||
},
|
||||
analysis: { "فیبر": "۲۵٪" },
|
||||
feedingAdvice: "بهتر است با آب ولرم خیسانده و سپس با غذا مخلوط شود.",
|
||||
specialist: {
|
||||
name: "دکتر بایر",
|
||||
title: "گاستروانترولوژیست",
|
||||
image: "https://images.unsplash.com/photo-1559839734-2b71f1536783?auto=format&fit=crop&q=80&w=200",
|
||||
message: "فیبر سیب منبع پکتین است که به رشد باکتریهای مفید روده کمک شایانی میکند."
|
||||
},
|
||||
image: "https://www.canina.de/media/4c/32/38/1715077271/Ballaststoff-Mix-140108-V1-Canina-100g_600x600.webp"
|
||||
},
|
||||
{
|
||||
id: "lachs-ol",
|
||||
artNo: "111803",
|
||||
name: "Lachs-Öl (روغن سالمون)",
|
||||
scientificTagline: "اسیدهای چرب ضروری با خلوص دارویی",
|
||||
description: "روغن سالمون ۱۰۰٪ خالص برای درخشش پوشش مویی.",
|
||||
shortDescription: "اکسیر درخشش و سلامت؛ روغن سالمون نروژی با امگا ۳ غلیظ برای رفع خشکی پوست و ریزش مو.",
|
||||
keyBenefits: [
|
||||
{ icon: "Flame", title: "پرس سرد", description: "حفظ حداکثری ویتامینها" },
|
||||
{ icon: "Zap", title: "امگا ۳ غلیظ", description: "تقویت قلب و سیستم ایمنی" },
|
||||
{ icon: "Star", title: "صد درصد طبیعی", description: "بدون هیچ افزودنی شیمیایی" }
|
||||
],
|
||||
category: "مراقبتهای ویژه (پوست، دندان و چشم)",
|
||||
categorySlug: "special-care",
|
||||
price: "۲,۲۴۰,۰۰۰ تومان",
|
||||
priceValue: 2240000,
|
||||
unit: "250 ml",
|
||||
packageSize: 250,
|
||||
main_ingredients: ["100% روغن سالمون خالص", "امگا 3 (EPA & DHA)"],
|
||||
dosage_logic: "2 میلیلیتر به ازای هر 10 کیلوگرم وزن بدن. حداکثر 10 میلیلیتر.",
|
||||
benefits: "پشتیبانی از بازسازی پوست، رشد موی براق و تقویت متابولیسم.",
|
||||
symptoms: ["ریزش مو", "خشکی پوست", "خارش"],
|
||||
suitableFor: "هر دو",
|
||||
onSetOfAction: "هفته سوم",
|
||||
optimisticTemplate: "شاهد درخشش خیرهکننده در پوشش موی [PetName] از [OnSet] باشید.",
|
||||
expectedResults: [
|
||||
{ icon: "Sparkles", text: "موی براقتر" },
|
||||
{ icon: "Heart", text: "پوست سالم" },
|
||||
{ icon: "ShieldCheck", text: "کاهش ریزش" }
|
||||
],
|
||||
calculateDosage: (weight) => {
|
||||
let ml = (weight / 10) * 2;
|
||||
ml = Math.min(ml, 10);
|
||||
return { quantity: Number(ml.toFixed(1)), unit: "میلیلیتر", description: "دوز روزانه" };
|
||||
},
|
||||
analysis: { "چربی خام": "۹۹٪" },
|
||||
feedingAdvice: "روزانه به غذای اصلی پت اضافه شود.",
|
||||
specialist: {
|
||||
name: "دکتر ماری کونیگ",
|
||||
title: "متخصص تغذیه دامپزشکی",
|
||||
image: "https://images.unsplash.com/photo-1559839734-2b71f1536783?auto=format&fit=crop&q=80&w=200",
|
||||
message: "روغن سالمون کانینا به روش پرس سرد تهیه شده تا تمامی ویتامینهای حساس به حرارت حفظ شوند."
|
||||
},
|
||||
image: "https://www.canina.de/media/6b/48/81/731000_Canina_Lachsoel_Lachs_Oel_1280x1280.png"
|
||||
},
|
||||
{
|
||||
id: "herz-vital",
|
||||
artNo: "111858",
|
||||
name: "Herz Vital (هرز ویتال)",
|
||||
scientificTagline: "محافظت تخصصی از میوکارد و سیستم گردش خون",
|
||||
description: "حمایت از عملکرد قلب در سگها و گربهها.",
|
||||
shortDescription: "موتور تپنده زندگی؛ مکمل تخصصی حاوی ال-کارنیتین و زالزالک برای تقویت عضله قلب در پتهای مسن و کاری.",
|
||||
keyBenefits: [
|
||||
{ icon: "Heart", title: "تقویت میوکارد", description: "بهبود پمپاژ خون" },
|
||||
{ icon: "Activity", title: "نشاط فیزیکی", description: "کاهش خستگی زودرس" },
|
||||
{ icon: "Zap", title: "ال-کارنیتین", description: "تامین انرژی سلولی قلب" }
|
||||
],
|
||||
category: "ویتامینها و انرژیبخشها",
|
||||
categorySlug: "energy",
|
||||
price: "۴,۲۸۵,۰۰۰ تومان",
|
||||
priceValue: 4285000,
|
||||
unit: "100 ml",
|
||||
packageSize: 100,
|
||||
main_ingredients: ["ال-کارنیتین", "زالزالک", "عصاره مخمر"],
|
||||
dosage_logic: "0.5 قاشق چایخوری (تقریباً 1.5ml) به ازای هر 10kg وزن. حداکثر 3 قاشق چایخوری.",
|
||||
benefits: "تقویت عملکرد فیزیولوژی قلب و افزایش انرژی در سگها و گربههای مسن.",
|
||||
symptoms: ["نفسنفس زدن", "خستگی", "سن بالا"],
|
||||
suitableFor: "هر دو",
|
||||
calculateDosage: (weight) => {
|
||||
let tsp = (weight / 10) * 0.5;
|
||||
tsp = Math.min(tsp, 3);
|
||||
return { quantity: Number(tsp.toFixed(1)), unit: "قاشق چایخوری", description: "معادل " + (tsp * 3).toFixed(1) + " میلیلیتر" };
|
||||
},
|
||||
analysis: { "ال-کارنیتین": "۳۰۰۰۰mg" },
|
||||
feedingAdvice: "قبل از مصرف تکان دهید.",
|
||||
specialist: {
|
||||
name: "دکتر مایر",
|
||||
title: "کاردیولوژیست دامپزشکی",
|
||||
image: "https://images.unsplash.com/photo-1559839734-2b71f1536783?auto=format&fit=crop&q=80&w=200",
|
||||
message: "زالزالک جریان خون کرونر را بهبود بخشیده و فشار بر قلب را کاهش میدهد."
|
||||
},
|
||||
image: "https://www.canina.de/media/73/6e/8b/1715077271/Herz-Vital-111858-V1-Canina-100ml_600x600.webp"
|
||||
},
|
||||
{
|
||||
id: "energy-gel",
|
||||
artNo: "111452",
|
||||
name: "Petvital Energy-Gel (پتویتال انرژی ژل)",
|
||||
scientificTagline: "تامین فوری گلوکز و ویتامینهای متابولیک برای بازیابی توان",
|
||||
description: "کمکدرمانی تخصصی برای زمانهای بیاشتهایی، نقاهت و دورههای پراسترس سگ و گربه.",
|
||||
shortDescription: "منبع انرژی متمرکز و خوشخوراک؛ حاوی ویتامینهای ب-کمپلکس و گلوکز برای بازگشت سریع انرژی در دوران نقاهت.",
|
||||
category: "ویتامینها و انرژیبخشها",
|
||||
categorySlug: "energy",
|
||||
price: "۲,۵۰۰,۰۰۰ تومان",
|
||||
priceValue: 2500000,
|
||||
unit: "100 g",
|
||||
packageSize: 100,
|
||||
main_ingredients: ["ویتامینهای گروه B", "ویتامین E", "کنسانتره انرژی", "منیزیم"],
|
||||
dosage_logic: "۱ قاشق چایخوری به ازای هر ۱۰kg وزن. حداکثر ۴ قاشق چایخوری.",
|
||||
benefits: "تامین سریع انرژی در زمان بیاشتهایی، نقاهت بعد از جراحی، دورههای بارداری و شیردهی و مسابقات ورزشی.",
|
||||
symptoms: ["بیاشتهایی", "نقاهت بعد جراحی", "خستگی مزمن", "کاهش وزن ناگهانی"],
|
||||
suitableFor: "هر دو",
|
||||
onSetOfAction: "بلافاصله",
|
||||
optimisticTemplate: "[PetName] بلافاصله پس از مصرف انرژی مضاعفی برای فعالیتهای روزانه پیدا میکند.",
|
||||
expectedResults: [
|
||||
{ icon: "Zap", text: "رفع فوری خستگی" },
|
||||
{ icon: "Activity", text: "بهبود اشتها" },
|
||||
{ icon: "Heart", text: "نشاط متابولیک" }
|
||||
],
|
||||
calculateDosage: (weight) => {
|
||||
let qty = Math.max(1, weight / 10);
|
||||
qty = Math.min(qty, 4);
|
||||
return { quantity: Math.round(qty), unit: "قاشق چایخوری", description: "بمب انرژی" };
|
||||
},
|
||||
analysis: { "ویتامین B1": "400mg" },
|
||||
feedingAdvice: "به دلیل غلظت بالا، به تنهایی یا با غذا داده شود.",
|
||||
specialist: {
|
||||
name: "دکتر اشمیت",
|
||||
title: "متخصص تغذیه بالینی",
|
||||
image: "https://images.unsplash.com/photo-1559839734-2b71f1536783?auto=format&fit=crop&q=80&w=200",
|
||||
message: "این ژل برای پتهایی که در شرایط بحرانی نیاز به کالری فوزی دارند بینظیر است."
|
||||
},
|
||||
image: "https://www.canina.de/media/cc/e0/75/1715077271/Petvital-Energy-Gel-111452-V1-Canina-100g_600x600.webp"
|
||||
},
|
||||
{
|
||||
id: "moor-tranke",
|
||||
artNo: "112404",
|
||||
name: "Moor-Tränke (عصاره پیت)",
|
||||
scientificTagline: "اکسیر طبیعی بر پایه اسیدهای هومیک گیاهی",
|
||||
description: "اکسیر طبیعی برای سلامت گوارش.",
|
||||
shortDescription: "سمزدای قدرتمند طبیعت؛ عصاره پیت حاوی اسیدهای هومیک برای پاکسازی روده از توکسینها و تنظیم اشتها.",
|
||||
keyBenefits: [
|
||||
{ icon: "Activity", title: "اسید هومیک", description: "دفع سموم و فلزات سنگین" },
|
||||
{ icon: "ShieldCheck", title: "ترمیم مخاط", description: "ایجاد لایه محافظ در روده" },
|
||||
{ icon: "Sparkles", title: "اشتا آور", description: "بهبود جذب مواد مغذی" }
|
||||
],
|
||||
category: "تقویت سیستم ایمنی و گوارش",
|
||||
categorySlug: "immune",
|
||||
price: "۳,۳۱۰,۰۰۰ تومان",
|
||||
priceValue: 3310000,
|
||||
unit: "250 ml",
|
||||
packageSize: 250,
|
||||
main_ingredients: ["100% عصاره گیاهی پیت (Peat Extract)", "اسیدهای هومیک"],
|
||||
dosage_logic: "تا 5kg: مقدار 2.5ml. تا 10kg: مقدار 5ml. تا 20kg: مقدار 10ml. بالای 35kg: مقدار 20ml.",
|
||||
benefits: "محافظت از غشاهای مخاطی دستگاه گوارش، تحریک اشتها و تقویت سیستم ایمنی روده.",
|
||||
symptoms: ["اسهال", "سمزدایی", "تقویت روده"],
|
||||
suitableFor: "هر دو",
|
||||
calculateDosage: (weight) => {
|
||||
let ml = 2.5;
|
||||
if (weight <= 5) ml = 2.5;
|
||||
else if (weight <= 10) ml = 5;
|
||||
else if (weight <= 20) ml = 10;
|
||||
else ml = 20;
|
||||
return { quantity: ml, unit: "میلیلیتر", description: "قبل از مصرف تکان دهید" };
|
||||
},
|
||||
analysis: { "اسید هومیک": "بالا" },
|
||||
feedingAdvice: "روزانه با آب آشامیدنی یا غذا مخلوط شود.",
|
||||
specialist: {
|
||||
name: "دکتر کوخ",
|
||||
title: "متخصص گوارش دامپزشکی",
|
||||
image: "https://images.unsplash.com/photo-1559839734-2b71f1536783?auto=format&fit=crop&q=80&w=200",
|
||||
message: "اسیدهای هومیک سموم را در روده به دام انداخته و دفع میکنند."
|
||||
},
|
||||
image: "https://www.canina.de/media/89/3b/bc/112404_Moortraenke_Canina-Pharma_1280x1280.png",
|
||||
faqs: [
|
||||
{ question: "آیا برای اسهال مزمن موثر است؟", answer: "بله، اسیدهای هومیک موجود در این محصول با ایجاد لایه محافظ روی مخاط روده و جذب سموم، به سرعت وضعیت دفع را بهبود میبخند." },
|
||||
{ question: "نحوه نگهداری محصول چگونه است؟", answer: "پس از باز شدن درب، در جای خنک (یخچال نباشد) نگهداری شود و قبل از هر بار مصرف به خوبی تکان دهید." }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "schwarz-kummel",
|
||||
artNo: "112503",
|
||||
name: "Schwarz-Kümmel (روغن سیاه دانه)",
|
||||
scientificTagline: "درکان سنتی ارتقا یافته با تکنولوژی آلمانی",
|
||||
description: "روغن سیاه دانه مصری ۱۰۰٪ خالص برای تقویت سیستم ایمنی و متابولیسم.",
|
||||
shortDescription: "درمان سنتی کهن؛ روغن سیاه دانه مصری با پرس سرد برای مقابله با آلرژی و دفع طبیعی پارازیتها.",
|
||||
keyBenefits: [
|
||||
{ icon: "Flame", title: "پرس سرد", description: "خلوص ۱۰۰ درصد دارویی" },
|
||||
{ icon: "ShieldCheck", title: "ضد انگل", description: "دفاع طبیعی در برابر کک و کنه" },
|
||||
{ icon: "Zap", title: "تقویت متابولیک", description: "بهبود سیستم تنفسی و ایمنی" }
|
||||
],
|
||||
category: "تقویت عمومی",
|
||||
categorySlug: "general",
|
||||
price: "۱,۴۵۰,۰۰۰ تومان",
|
||||
priceValue: 1450000,
|
||||
unit: "250 ml",
|
||||
packageSize: 250,
|
||||
main_ingredients: ["۱۰۰٪ روغن سیاه دانه مصری"],
|
||||
dosage_logic: "۰.۳ میلیلیتر به ازای هر کیلوگرم وزن بدن.",
|
||||
benefits: "بهبود کیفیت پوست و مو، مقابله با انگلها و تقویت کلی سیستم دفاعی.",
|
||||
symptoms: ["ضعف سیستم ایمنی", "الرژی", "انگلهای خارجی"],
|
||||
suitableFor: "سگ",
|
||||
contraindications: ["برای سگهای باردار استفاده نشود", "برای سگهای شیرده استفاده نشود"],
|
||||
calculateDosage: (weight) => {
|
||||
const ml = weight * 0.3;
|
||||
return { quantity: Number(ml.toFixed(1)), unit: "میلیلیتر", description: "به همراه غذا میل شود" };
|
||||
},
|
||||
analysis: { "چربی خالص": "۱۰۰٪" },
|
||||
feedingAdvice: "به دلیل غلظت بالای تیموکینون، دقیقاً مطابق دوز مصرف شود.",
|
||||
specialist: {
|
||||
name: "دکتر مونیخ",
|
||||
title: "متخصص طب طبیعی دامپزشکی",
|
||||
image: "https://images.unsplash.com/photo-1559839734-2b71f1536783?auto=format&fit=crop&q=80&w=200",
|
||||
message: "روغن سیاه دانه مصری کانینا حاوی مقادیر بالایی اسیدهای چرب چند غیراشباع است."
|
||||
},
|
||||
image: "https://www.canina.de/media/1b/83/7d/1715077272/Schwarz-Kuemmel-112503-V1-Canina-250ml_600x600.webp"
|
||||
}
|
||||
];
|
||||
43
frontend/application/lib/data/scientificTerms.ts
Normal file
43
frontend/application/lib/data/scientificTerms.ts
Normal file
@ -0,0 +1,43 @@
|
||||
export interface ScientificTerm {
|
||||
term: string;
|
||||
definition: string;
|
||||
wikiId?: string;
|
||||
}
|
||||
|
||||
export const SCIENTIFIC_TERMS: Record<string, ScientificTerm> = {
|
||||
"گلیکوزآمینوگلیکان": {
|
||||
term: "گلیکوزآمینوگلیکان (GAG)",
|
||||
definition: "ترکیبات ساختاری ضروری برای بازسازی غضروفها و مایع مفصلی که در Canhydrox GAG به وفور یافت میشود.",
|
||||
wikiId: "gag"
|
||||
},
|
||||
"اسیدهای هومیک": {
|
||||
term: "اسیدهای هومیک",
|
||||
definition: "مواد آلی طبیعی موجود در عصاره پیت (Moor) که از مخاط روده محافظت کرده و سموم را جذب میکنند.",
|
||||
wikiId: "humic-acids"
|
||||
},
|
||||
"کلاژن زیستفعال": {
|
||||
term: "کلاژن زیستفعال",
|
||||
definition: "پروتئینهای اختصاصی هیدرولیز شده که به سرعت جذب استخوان و تاندون شده و استحکام ساختاری ایجاد میکنند.",
|
||||
wikiId: "collagen"
|
||||
},
|
||||
"تیموکینون": {
|
||||
term: "تیموکینون",
|
||||
definition: "ماده موثره اصلی در روغن سیاه دانه با خواص ضدالتهابی و ضدآلرژی بسیار قوی.",
|
||||
wikiId: "thymoquinone"
|
||||
},
|
||||
"امگا ۳": {
|
||||
term: "امگا ۳",
|
||||
definition: "اسیدهای چرب ضروری که برای سلامت پوست، مو و عملکرد قلب حیاتی هستند.",
|
||||
wikiId: "omega3"
|
||||
},
|
||||
"میکروسیلور": {
|
||||
term: "نقره میکروسیلور (Microsilver BG)",
|
||||
definition: "ذرات نقره خالص با ساختار اسفنجی که بدون جذب در خون، اثر آنتیباکتریال پایداری بر سطح پوست و لثه دارند.",
|
||||
wikiId: "silver"
|
||||
},
|
||||
"صدف لبسبز": {
|
||||
term: "صدف لبسبز (Green-lipped Mussel)",
|
||||
definition: "صدف بومی نیوزلند حاوی گلوکوزامینگلیکانهای طبیعی که نقش حیاتی در بازسازی غضروف و مفاصل دارند.",
|
||||
wikiId: "joints"
|
||||
}
|
||||
};
|
||||
20
frontend/application/lib/hooks/useNetworkStatus.ts
Normal file
20
frontend/application/lib/hooks/useNetworkStatus.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useNetworkStatus() {
|
||||
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
||||
|
||||
useEffect(() => {
|
||||
const handleOnline = () => setIsOnline(true);
|
||||
const handleOffline = () => setIsOnline(false);
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isOnline;
|
||||
}
|
||||
23
frontend/application/lib/services/api.ts
Normal file
23
frontend/application/lib/services/api.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Interceptor to add auth token in the future
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
export default api;
|
||||
102
frontend/application/lib/services/authService.ts
Normal file
102
frontend/application/lib/services/authService.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import api from './api';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
mobile: string | null;
|
||||
role: string;
|
||||
walletBalance: number;
|
||||
charityDonationTotal: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
pets?: any[];
|
||||
orders?: any[];
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
user: User;
|
||||
accessToken: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class AuthService {
|
||||
private static instance: AuthService;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): AuthService {
|
||||
if (!AuthService.instance) {
|
||||
AuthService.instance = new AuthService();
|
||||
}
|
||||
return AuthService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send SMS verification code (OTP)
|
||||
*/
|
||||
public async sendOtp(phoneNumber: string): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await api.post('/auth/send-otp', { phoneNumber });
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message || 'خطا در ارسال کد تایید';
|
||||
throw new Error(Array.isArray(message) ? message[0] : message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify SMS verification code and login/register
|
||||
*/
|
||||
public async verifyOtp(phoneNumber: string, code: string): Promise<AuthResponse> {
|
||||
try {
|
||||
const response = await api.post('/auth/verify-otp', { phoneNumber, code });
|
||||
const { data } = response.data;
|
||||
if (data?.accessToken) {
|
||||
localStorage.setItem('accessToken', data.accessToken);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message || 'کد تایید نامعتبر است';
|
||||
throw new Error(Array.isArray(message) ? message[0] : message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user profile
|
||||
*/
|
||||
public async getProfile(): Promise<User> {
|
||||
try {
|
||||
const response = await api.get('/users/profile');
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message || 'خطا در دریافت اطلاعات کاربری';
|
||||
throw new Error(Array.isArray(message) ? message[0] : message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current user profile
|
||||
*/
|
||||
public async updateProfile(profileData: Partial<User>): Promise<User> {
|
||||
try {
|
||||
const response = await api.patch('/users/profile', profileData);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message || 'خطا در ویرایش اطلاعات کاربری';
|
||||
throw new Error(Array.isArray(message) ? message[0] : message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user and clear tokens
|
||||
*/
|
||||
public logout(): void {
|
||||
localStorage.removeItem('accessToken');
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = AuthService.getInstance();
|
||||
79
frontend/application/lib/services/orderService.ts
Normal file
79
frontend/application/lib/services/orderService.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import api from './api';
|
||||
import { CartItem } from '../store/cartStore';
|
||||
|
||||
export interface OrderItem {
|
||||
id: string;
|
||||
orderId: string;
|
||||
productId: string | null;
|
||||
quantity: number;
|
||||
product?: any;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
userId: string;
|
||||
couponId: string | null;
|
||||
totalAmount: number;
|
||||
charityDonation: number;
|
||||
status: string;
|
||||
trackingNumber: string | null;
|
||||
createdAt: string;
|
||||
orderItems: OrderItem[];
|
||||
}
|
||||
|
||||
export class OrderService {
|
||||
private static instance: OrderService;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): OrderService {
|
||||
if (!OrderService.instance) {
|
||||
OrderService.instance = new OrderService();
|
||||
}
|
||||
return OrderService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new order on the backend
|
||||
*/
|
||||
public async createOrder(orderData: {
|
||||
petId?: string;
|
||||
items: { productId: string; quantity: number }[];
|
||||
}): Promise<Order> {
|
||||
try {
|
||||
const response = await api.post('/orders', orderData);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message || 'خطا در ثبت سفارش';
|
||||
throw new Error(Array.isArray(message) ? message[0] : message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all orders for the current user
|
||||
*/
|
||||
public async getUserOrders(): Promise<Order[]> {
|
||||
try {
|
||||
const response = await api.get('/orders');
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message || 'خطا در دریافت لیست سفارشها';
|
||||
throw new Error(Array.isArray(message) ? message[0] : message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get order details by ID
|
||||
*/
|
||||
public async getOrderById(id: string): Promise<Order> {
|
||||
try {
|
||||
const response = await api.get(`/orders/${id}`);
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message || 'خطا در دریافت جزئیات سفارش';
|
||||
throw new Error(Array.isArray(message) ? message[0] : message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const orderService = OrderService.getInstance();
|
||||
106
frontend/application/lib/services/productService.ts
Normal file
106
frontend/application/lib/services/productService.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import api from './api';
|
||||
import { PRODUCTS, Product, PetType } from "../data/products";
|
||||
|
||||
export class ProductService {
|
||||
private static instance: ProductService;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): ProductService {
|
||||
if (!ProductService.instance) {
|
||||
ProductService.instance = new ProductService();
|
||||
}
|
||||
return ProductService.instance;
|
||||
}
|
||||
|
||||
private mapBackendToFrontend(data: any): Product {
|
||||
// Find the local static product to inherit functions like calculateDosage
|
||||
const local = PRODUCTS.find(p => p.artNo === data.artNo);
|
||||
|
||||
const safeParse = (val: any, fallback: any) => {
|
||||
if (!val) return fallback;
|
||||
if (typeof val === 'string') {
|
||||
try { return JSON.parse(val); } catch (e) { return fallback; }
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
artNo: data.artNo,
|
||||
name: data.name,
|
||||
scientificTagline: data.scientificTagline,
|
||||
description: data.description,
|
||||
shortDescription: data.shortDescription,
|
||||
price: data.priceDisplay || `${data.priceValue.toLocaleString('fa-IR')} تومان`,
|
||||
priceValue: Number(data.priceValue),
|
||||
category: data.category,
|
||||
categorySlug: data.categorySlug,
|
||||
unit: data.unit,
|
||||
packageSize: data.packageSize,
|
||||
dosage_logic: data.dosageLogic,
|
||||
benefits: data.benefits,
|
||||
suitableFor: data.suitableFor as PetType,
|
||||
storage: data.storage,
|
||||
specialBadge: data.specialBadge,
|
||||
onSetOfAction: data.onSetOfAction,
|
||||
optimisticTemplate: data.optimisticTemplate,
|
||||
feedingAdvice: data.feedingAdvice,
|
||||
image: data.image,
|
||||
|
||||
main_ingredients: data.ingredients?.map((i: any) => i.ingredient) || [],
|
||||
symptoms: data.symptoms?.map((s: any) => s.symptom) || [],
|
||||
keyBenefits: safeParse(data.keyBenefits, []),
|
||||
expectedResults: safeParse(data.expectedResults, []),
|
||||
benefitsList: safeParse(data.benefitsList, []),
|
||||
faqs: safeParse(data.faqs, []),
|
||||
analysis: safeParse(data.analysis, {}),
|
||||
specialist: safeParse(data.specialist, null),
|
||||
relatedProducts: safeParse(data.relatedProducts, []),
|
||||
contraindications: safeParse(data.contraindications, []),
|
||||
|
||||
calculateDosage: local?.calculateDosage || (() => ({ quantity: 0, unit: '', description: '' }))
|
||||
};
|
||||
}
|
||||
|
||||
public async getProducts(filters?: {
|
||||
category?: string;
|
||||
petType?: PetType | "all";
|
||||
query?: string;
|
||||
}): Promise<Product[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.category && filters.category !== "all") params.append('category', filters.category);
|
||||
if (filters?.petType && filters.petType !== "all") params.append('petType', filters.petType);
|
||||
if (filters?.query) params.append('query', filters.query);
|
||||
|
||||
try {
|
||||
const response = await api.get(`/products?${params.toString()}`);
|
||||
return response.data.map((item: any) => this.mapBackendToFrontend(item));
|
||||
} catch (error) {
|
||||
console.error("[ProductService] Failed to fetch products:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async getProductById(id: string): Promise<Product | null> {
|
||||
try {
|
||||
const response = await api.get(`/products/${id}`);
|
||||
return this.mapBackendToFrontend(response.data);
|
||||
} catch (error) {
|
||||
console.error(`[ProductService] Failed to fetch product ${id}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async getFeaturedProducts(): Promise<Product[]> {
|
||||
try {
|
||||
const response = await api.get('/products');
|
||||
return response.data.slice(0, 4).map((item: any) => this.mapBackendToFrontend(item));
|
||||
} catch (error) {
|
||||
console.error("[ProductService] Failed to fetch featured products:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const productService = ProductService.getInstance();
|
||||
142
frontend/application/lib/store/__tests__/cartStore.test.ts
Normal file
142
frontend/application/lib/store/__tests__/cartStore.test.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { useCartStore } from '../cartStore';
|
||||
import { orderService } from '../../services/orderService';
|
||||
|
||||
vi.mock('../../services/orderService', () => ({
|
||||
orderService: {
|
||||
createOrder: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockProduct = {
|
||||
id: 'prod-1',
|
||||
artNo: 'art-1',
|
||||
name: 'Canhydrox GAG',
|
||||
scientificTagline: 'Joint helper',
|
||||
description: 'Joints',
|
||||
price: '۱,۰۰۰ تومان',
|
||||
priceValue: 1000,
|
||||
category: 'joints',
|
||||
unit: 'tablet',
|
||||
packageSize: 100,
|
||||
main_ingredients: [],
|
||||
dosage_logic: '1',
|
||||
benefits: '',
|
||||
symptoms: [],
|
||||
suitableFor: 'سگ' as any,
|
||||
calculateDosage: () => ({ quantity: 1, unit: 'tablet', description: '' }),
|
||||
analysis: {},
|
||||
feedingAdvice: '',
|
||||
specialist: null as any,
|
||||
image: '',
|
||||
};
|
||||
|
||||
describe('cartStore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useCartStore.setState({
|
||||
items: [],
|
||||
isSubscribed: false,
|
||||
charityDonation: 0,
|
||||
coupon: null,
|
||||
orders: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add item to cart', () => {
|
||||
const store = useCartStore.getState();
|
||||
store.addItem(mockProduct, 2);
|
||||
|
||||
const state = useCartStore.getState();
|
||||
expect(state.items).toHaveLength(1);
|
||||
expect(state.items[0].product.id).toBe('prod-1');
|
||||
expect(state.items[0].quantity).toBe(2);
|
||||
});
|
||||
|
||||
it('should increment quantity if item already in cart', () => {
|
||||
const store = useCartStore.getState();
|
||||
store.addItem(mockProduct, 2);
|
||||
store.addItem(mockProduct, 3);
|
||||
|
||||
const state = useCartStore.getState();
|
||||
expect(state.items).toHaveLength(1);
|
||||
expect(state.items[0].quantity).toBe(5);
|
||||
});
|
||||
|
||||
it('should remove item from cart', () => {
|
||||
const store = useCartStore.getState();
|
||||
store.addItem(mockProduct, 2);
|
||||
store.removeItem('prod-1');
|
||||
|
||||
const state = useCartStore.getState();
|
||||
expect(state.items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should update quantity', () => {
|
||||
const store = useCartStore.getState();
|
||||
store.addItem(mockProduct, 2);
|
||||
store.updateQuantity('prod-1', 4);
|
||||
|
||||
const state = useCartStore.getState();
|
||||
expect(state.items[0].quantity).toBe(4);
|
||||
});
|
||||
|
||||
it('should remove item if update quantity is <= 0', () => {
|
||||
const store = useCartStore.getState();
|
||||
store.addItem(mockProduct, 2);
|
||||
store.updateQuantity('prod-1', 0);
|
||||
|
||||
const state = useCartStore.getState();
|
||||
expect(state.items).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should apply discount coupon', () => {
|
||||
const store = useCartStore.getState();
|
||||
const success = store.applyCoupon('CANINA2024');
|
||||
expect(success).toBe(true);
|
||||
|
||||
const state = useCartStore.getState();
|
||||
expect(state.coupon).toEqual({ code: 'CANINA2024', discount: 0.1 });
|
||||
});
|
||||
|
||||
it('should calculate subtotal, discount and total', () => {
|
||||
const store = useCartStore.getState();
|
||||
store.addItem(mockProduct, 2); // 2 * 1000 = 2000
|
||||
store.applyCoupon('CANINA2024'); // 10% discount
|
||||
|
||||
expect(useCartStore.getState().getSubtotal()).toBe(2000);
|
||||
expect(useCartStore.getState().getDiscount()).toBe(200);
|
||||
expect(useCartStore.getState().getTotal()).toBe(1800);
|
||||
});
|
||||
|
||||
it('should create order successfully via orderService', async () => {
|
||||
const mockBackendOrder = {
|
||||
id: 'backend-order-id',
|
||||
createdAt: '2026-05-26T18:00:00.000Z',
|
||||
totalAmount: '2000.00',
|
||||
charityDonation: '0.00',
|
||||
status: 'processing',
|
||||
trackingNumber: 'TRK-12345',
|
||||
};
|
||||
|
||||
vi.mocked(orderService.createOrder).mockResolvedValue(mockBackendOrder);
|
||||
|
||||
const store = useCartStore.getState();
|
||||
store.addItem(mockProduct, 2);
|
||||
|
||||
const orderId = await store.addOrder({
|
||||
items: store.items,
|
||||
total: 2000,
|
||||
charityDonation: 0,
|
||||
petId: 'pet-id',
|
||||
});
|
||||
|
||||
expect(orderService.createOrder).toHaveBeenCalled();
|
||||
expect(orderId).toBe('backend-order-id');
|
||||
|
||||
const state = useCartStore.getState();
|
||||
expect(state.orders).toHaveLength(1);
|
||||
expect(state.orders[0].id).toBe('backend-order-id');
|
||||
expect(state.orders[0].total).toBe(2000);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { useSettingsStore } from '../settingsStore';
|
||||
import api from '../../services/api';
|
||||
|
||||
vi.mock('../../services/api', () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('settingsStore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useSettingsStore.setState({
|
||||
texts: {},
|
||||
scientificTerms: {},
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return fallback if key is not found', () => {
|
||||
const text = useSettingsStore.getState().getText('non-existent', 'Fallback Text');
|
||||
expect(text).toBe('Fallback Text');
|
||||
});
|
||||
|
||||
it('should return value if key is found', () => {
|
||||
useSettingsStore.setState({
|
||||
texts: { 'test-key': 'Real Value' },
|
||||
});
|
||||
const text = useSettingsStore.getState().getText('test-key', 'Fallback Text');
|
||||
expect(text).toBe('Real Value');
|
||||
});
|
||||
|
||||
it('should fetch settings successfully and map array to object', async () => {
|
||||
const mockUiTexts = [
|
||||
{ key: 'hero_title', value: 'Welcome to Canina' },
|
||||
{ key: 'hero_desc', value: 'Best supplements' },
|
||||
];
|
||||
const mockTerms = [
|
||||
{ key: 'mussel', term: 'Mussel', definition: 'NZ Green Mussel', wikiId: 'general' },
|
||||
];
|
||||
|
||||
vi.mocked(api.get).mockImplementation((url: string) => {
|
||||
if (url === '/settings/ui-texts') {
|
||||
return Promise.resolve({ data: mockUiTexts });
|
||||
}
|
||||
if (url === '/settings/scientific-terms') {
|
||||
return Promise.resolve({ data: mockTerms });
|
||||
}
|
||||
return Promise.reject(new Error('Unknown URL'));
|
||||
});
|
||||
|
||||
const store = useSettingsStore.getState();
|
||||
await store.fetchSettings();
|
||||
|
||||
const state = useSettingsStore.getState();
|
||||
expect(state.texts['hero_title']).toBe('Welcome to Canina');
|
||||
expect(state.texts['hero_desc']).toBe('Best supplements');
|
||||
expect(state.scientificTerms['mussel']).toEqual(mockTerms[0]);
|
||||
expect(state.isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
133
frontend/application/lib/store/__tests__/userStore.test.ts
Normal file
133
frontend/application/lib/store/__tests__/userStore.test.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { useUserStore } from '../userStore';
|
||||
import { authService } from '../../services/authService';
|
||||
import api from '../../services/api';
|
||||
|
||||
vi.mock('../../services/authService', () => ({
|
||||
authService: {
|
||||
updateProfile: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
getProfile: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../services/api', () => ({
|
||||
default: {
|
||||
post: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('userStore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useUserStore.setState({
|
||||
role: 'User_Guest',
|
||||
isLoggedIn: false,
|
||||
profile: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
mobile: '',
|
||||
walletBalance: 0,
|
||||
charityDonationTotal: 0,
|
||||
addresses: [],
|
||||
transactions: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should logout correctly', () => {
|
||||
useUserStore.setState({
|
||||
role: 'User_PetOwner',
|
||||
isLoggedIn: true,
|
||||
profile: {
|
||||
firstName: 'Ali',
|
||||
lastName: 'Ahmadi',
|
||||
email: 'ali@example.com',
|
||||
mobile: '0912',
|
||||
walletBalance: 100,
|
||||
charityDonationTotal: 10,
|
||||
addresses: [],
|
||||
transactions: [],
|
||||
},
|
||||
});
|
||||
|
||||
useUserStore.getState().logout();
|
||||
|
||||
expect(authService.logout).toHaveBeenCalled();
|
||||
const state = useUserStore.getState();
|
||||
expect(state.isLoggedIn).toBe(false);
|
||||
expect(state.role).toBe('User_Guest');
|
||||
expect(state.profile.firstName).toBe('');
|
||||
});
|
||||
|
||||
it('should fetch profile successfully', async () => {
|
||||
const mockProfileData = {
|
||||
firstName: 'Ali',
|
||||
lastName: 'Ahmadi',
|
||||
email: 'ali@example.com',
|
||||
mobile: '09123456789',
|
||||
role: 'User_PetOwner',
|
||||
walletBalance: '1500.00',
|
||||
charityDonationTotal: '50.00',
|
||||
addresses: [],
|
||||
pets: [],
|
||||
walletTransactions: [],
|
||||
};
|
||||
|
||||
vi.mocked(authService.getProfile).mockResolvedValue(mockProfileData);
|
||||
|
||||
const store = useUserStore.getState();
|
||||
await store.fetchProfile();
|
||||
|
||||
expect(authService.getProfile).toHaveBeenCalled();
|
||||
const state = useUserStore.getState();
|
||||
expect(state.isLoggedIn).toBe(true);
|
||||
expect(state.role).toBe('User_PetOwner');
|
||||
expect(state.profile.firstName).toBe('Ali');
|
||||
expect(state.profile.walletBalance).toBe(1500);
|
||||
});
|
||||
|
||||
it('should update profile correctly', async () => {
|
||||
const mockUpdatedData = {
|
||||
firstName: 'Ali Updated',
|
||||
lastName: 'Ahmadi',
|
||||
email: 'ali@example.com',
|
||||
mobile: '09123456789',
|
||||
};
|
||||
|
||||
vi.mocked(authService.updateProfile).mockResolvedValue(mockUpdatedData);
|
||||
|
||||
const store = useUserStore.getState();
|
||||
await store.updateProfile({ firstName: 'Ali Updated' });
|
||||
|
||||
expect(authService.updateProfile).toHaveBeenCalledWith({ firstName: 'Ali Updated' });
|
||||
const state = useUserStore.getState();
|
||||
expect(state.profile.firstName).toBe('Ali Updated');
|
||||
});
|
||||
|
||||
it('should add address and call fetchProfile', async () => {
|
||||
const address = {
|
||||
id: 'addr-1',
|
||||
title: 'Home',
|
||||
receptorName: 'Ali',
|
||||
phone: '0912',
|
||||
province: 'Teh',
|
||||
city: 'Teh',
|
||||
detail: 'Det',
|
||||
zipCode: '123',
|
||||
isDefault: false,
|
||||
};
|
||||
|
||||
const fetchSpy = vi.spyOn(useUserStore.getState(), 'fetchProfile').mockResolvedValue(undefined);
|
||||
vi.mocked(api.post).mockResolvedValue({ data: { success: true } });
|
||||
|
||||
const store = useUserStore.getState();
|
||||
await store.addAddress(address);
|
||||
|
||||
expect(api.post).toHaveBeenCalled();
|
||||
expect(fetchSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
170
frontend/application/lib/store/cartStore.ts
Normal file
170
frontend/application/lib/store/cartStore.ts
Normal file
@ -0,0 +1,170 @@
|
||||
"use client";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { Product } from "../data/products";
|
||||
import { orderService } from "../services/orderService";
|
||||
import api from "../services/api";
|
||||
|
||||
|
||||
export interface CartItem {
|
||||
product: Product;
|
||||
quantity: number;
|
||||
calculatedDose?: {
|
||||
quantity: number;
|
||||
unit: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
date: string;
|
||||
items: CartItem[];
|
||||
total: number;
|
||||
charityDonation?: number;
|
||||
status: 'processing' | 'shipped' | 'delivered';
|
||||
petId?: string;
|
||||
trackingNumber: string;
|
||||
}
|
||||
|
||||
interface CartStore {
|
||||
items: CartItem[];
|
||||
isSubscribed: boolean;
|
||||
charityDonation: number;
|
||||
coupon: { code: string; discount: number } | null;
|
||||
orders: Order[];
|
||||
addItem: (product: Product, quantity: number, dose?: { quantity: number; unit: string }) => void;
|
||||
removeItem: (productId: string) => void;
|
||||
updateQuantity: (productId: string, quantity: number) => void;
|
||||
toggleSubscription: () => void;
|
||||
setCharityDonation: (amount: number) => void;
|
||||
applyCoupon: (code: string) => Promise<boolean>;
|
||||
addOrder: (order: Omit<Order, 'id' | 'date' | 'status' | 'trackingNumber'>) => Promise<string>;
|
||||
clearCart: () => void;
|
||||
removeCoupon: () => void;
|
||||
setOrders: (orders: any[]) => void;
|
||||
getTotalItems: () => number;
|
||||
getSubtotal: () => number;
|
||||
getDiscount: () => number;
|
||||
getTotal: () => number;
|
||||
}
|
||||
|
||||
export const useCartStore = create<CartStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
items: [],
|
||||
isSubscribed: false,
|
||||
charityDonation: 0,
|
||||
coupon: null,
|
||||
orders: [],
|
||||
addItem: (product, quantity, dose) => {
|
||||
const items = get().items;
|
||||
const existing = items.find((i) => i.product.id === product.id);
|
||||
if (existing) {
|
||||
set({
|
||||
items: items.map((i) =>
|
||||
i.product.id === product.id ? { ...i, quantity: i.quantity + quantity } : i
|
||||
),
|
||||
});
|
||||
} else {
|
||||
set({ items: [...items, { product, quantity, calculatedDose: dose }] });
|
||||
}
|
||||
},
|
||||
removeItem: (productId) => {
|
||||
set({ items: get().items.filter((i) => i.product.id !== productId) });
|
||||
},
|
||||
updateQuantity: (productId, quantity) => {
|
||||
if (quantity <= 0) {
|
||||
get().removeItem(productId);
|
||||
return;
|
||||
}
|
||||
set({
|
||||
items: get().items.map((i) =>
|
||||
i.product.id === productId ? { ...i, quantity } : i
|
||||
),
|
||||
});
|
||||
},
|
||||
toggleSubscription: () => set({ isSubscribed: !get().isSubscribed }),
|
||||
setCharityDonation: (amount) => set({ charityDonation: amount }),
|
||||
applyCoupon: async (code) => {
|
||||
try {
|
||||
const cleanCode = code.toUpperCase().trim();
|
||||
// Assuming API returns { discountValue: number } or similar
|
||||
const response = await api.post('/cart/apply-coupon', { code: cleanCode });
|
||||
if (response.data && response.data.discountValue) {
|
||||
set({ coupon: { code: cleanCode, discount: response.data.discountValue } });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Failed to apply coupon:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
addOrder: async (orderData) => {
|
||||
const payload = {
|
||||
petId: orderData.petId,
|
||||
items: orderData.items.map(i => ({
|
||||
productId: i.product.id,
|
||||
quantity: i.quantity
|
||||
}))
|
||||
};
|
||||
|
||||
try {
|
||||
const backendOrder = await orderService.createOrder(payload);
|
||||
|
||||
const newOrder: Order = {
|
||||
...orderData,
|
||||
id: backendOrder.id,
|
||||
date: backendOrder.createdAt,
|
||||
total: Number(backendOrder.totalAmount),
|
||||
charityDonation: Number(backendOrder.charityDonation),
|
||||
status: backendOrder.status as any,
|
||||
trackingNumber: backendOrder.trackingNumber || `CN-${Math.floor(Math.random() * 90000) + 10000}`
|
||||
};
|
||||
|
||||
set({ orders: [newOrder, ...get().orders] });
|
||||
return backendOrder.id;
|
||||
} catch (error: any) {
|
||||
console.error("Order creation failed on backend:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
clearCart: () => set({ items: [], coupon: null }),
|
||||
removeCoupon: () => set({ coupon: null }),
|
||||
setOrders: (backendOrders) => {
|
||||
const mappedOrders: Order[] = backendOrders.map(bo => ({
|
||||
id: bo.id,
|
||||
date: bo.createdAt,
|
||||
items: bo.orderItems?.map((oi: any) => ({
|
||||
product: oi.product,
|
||||
quantity: oi.quantity
|
||||
})) || [],
|
||||
total: Number(bo.totalAmount),
|
||||
charityDonation: Number(bo.charityDonation),
|
||||
status: bo.status,
|
||||
trackingNumber: bo.trackingNumber || `CN-${Math.floor(Math.random() * 90000) + 10000}`
|
||||
}));
|
||||
set({ orders: mappedOrders });
|
||||
},
|
||||
getTotalItems: () => get().items.reduce((acc, item) => acc + item.quantity, 0),
|
||||
getSubtotal: () => get().items.reduce((acc, item) => acc + item.product.priceValue * item.quantity, 0),
|
||||
getDiscount: () => {
|
||||
const subtotal = get().getSubtotal();
|
||||
let discount = get().isSubscribed ? subtotal * 0.05 : 0;
|
||||
|
||||
if (get().coupon) {
|
||||
const c = get().coupon!;
|
||||
if (c.discount < 1) {
|
||||
discount += subtotal * c.discount;
|
||||
} else {
|
||||
discount += c.discount;
|
||||
}
|
||||
}
|
||||
|
||||
return Math.min(discount, subtotal);
|
||||
},
|
||||
getTotal: () => get().getSubtotal() - get().getDiscount() + get().charityDonation,
|
||||
}),
|
||||
{ name: "canina-cart" }
|
||||
)
|
||||
);
|
||||
60
frontend/application/lib/store/settingsStore.ts
Normal file
60
frontend/application/lib/store/settingsStore.ts
Normal file
@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
import { create } from "zustand";
|
||||
import api from "../services/api";
|
||||
|
||||
export interface ScientificTerm {
|
||||
key: string;
|
||||
term: string;
|
||||
definition: string;
|
||||
wikiId: string;
|
||||
}
|
||||
|
||||
interface SettingsStore {
|
||||
texts: Record<string, string>;
|
||||
scientificTerms: Record<string, ScientificTerm>;
|
||||
isLoading: boolean;
|
||||
fetchSettings: () => Promise<void>;
|
||||
getText: (key: string, fallback: string) => string;
|
||||
}
|
||||
|
||||
export const useSettingsStore = create<SettingsStore>()((set, get) => ({
|
||||
texts: {},
|
||||
scientificTerms: {},
|
||||
isLoading: false,
|
||||
fetchSettings: async () => {
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const [textsRes, termsRes] = await Promise.all([
|
||||
api.get('/settings/ui-texts'),
|
||||
api.get('/settings/scientific-terms'),
|
||||
]);
|
||||
|
||||
const textsObj: Record<string, string> = {};
|
||||
if (Array.isArray(textsRes.data)) {
|
||||
textsRes.data.forEach((item: { key: string; value: string }) => {
|
||||
textsObj[item.key] = item.value;
|
||||
});
|
||||
}
|
||||
|
||||
const termsObj: Record<string, ScientificTerm> = {};
|
||||
if (Array.isArray(termsRes.data)) {
|
||||
termsRes.data.forEach((item: ScientificTerm) => {
|
||||
termsObj[item.key] = item;
|
||||
});
|
||||
}
|
||||
|
||||
set({ texts: textsObj, scientificTerms: termsObj });
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch settings/ui-texts:", error);
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
getText: (key, fallback) => {
|
||||
const value = get().texts[key];
|
||||
if (value === undefined || value === null) {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}));
|
||||
211
frontend/application/lib/store/usePetStore.ts
Normal file
211
frontend/application/lib/store/usePetStore.ts
Normal file
@ -0,0 +1,211 @@
|
||||
"use client";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import api from "../services/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export interface Reminder {
|
||||
id: string;
|
||||
title: string;
|
||||
time: string;
|
||||
frequency: "روزانه" | "هفتگی";
|
||||
productId?: string;
|
||||
completedDates: string[]; // List of ISO dates when this was completed
|
||||
}
|
||||
|
||||
export interface HealthLog {
|
||||
id: string;
|
||||
date: string;
|
||||
appetite: "عالی" | "متوسط" | "کم";
|
||||
energy: "زیاد" | "نرمال" | "بیحال";
|
||||
digestion: "نرمال" | "حساس" | "مشکلدار";
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface PetConsumption {
|
||||
productId: string;
|
||||
packageSize: number;
|
||||
remaining: number;
|
||||
}
|
||||
|
||||
export interface PetProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "سگ" | "گربه";
|
||||
breed: string;
|
||||
age: number;
|
||||
weight: number;
|
||||
activityLevel: "کم" | "متوسط" | "زیاد";
|
||||
medicalConditions: string[];
|
||||
image?: string;
|
||||
reminders: Reminder[];
|
||||
logs: HealthLog[];
|
||||
consumptions: PetConsumption[];
|
||||
}
|
||||
|
||||
interface PetStore {
|
||||
pets: PetProfile[];
|
||||
activePetId: string | null;
|
||||
addPet: (pet: Omit<PetProfile, "id">) => void;
|
||||
removePet: (id: string) => void;
|
||||
setActivePet: (id: string) => void;
|
||||
getActivePet: () => PetProfile | null;
|
||||
updatePet: (id: string, updates: Partial<PetProfile>) => void;
|
||||
addReminder: (petId: string, reminder: Omit<Reminder, "id" | "completedDates">) => Promise<void>;
|
||||
toggleReminder: (petId: string, reminderId: string, date: string) => Promise<void>;
|
||||
addHealthLog: (petId: string, log: Omit<HealthLog, "id" | "date">) => Promise<void>;
|
||||
setPets: (pets: any[]) => void;
|
||||
}
|
||||
|
||||
export const usePetStore = create<PetStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
pets: [],
|
||||
activePetId: null,
|
||||
addPet: async (pet) => {
|
||||
const { useUserStore } = await import("./userStore");
|
||||
const isLoggedIn = useUserStore.getState().isLoggedIn;
|
||||
|
||||
if (isLoggedIn) {
|
||||
try {
|
||||
await api.post("/pets", {
|
||||
name: pet.name,
|
||||
type: pet.type,
|
||||
breed: pet.breed || "",
|
||||
weight: Number(pet.weight) || 0
|
||||
});
|
||||
await useUserStore.getState().fetchProfile();
|
||||
} catch (error) {
|
||||
console.error("Failed to add pet via backend API:", error);
|
||||
toast.error("خطا در ثبت پت در سرور");
|
||||
}
|
||||
} else {
|
||||
const id = Math.random().toString(36).substring(7);
|
||||
const newPet = {
|
||||
...pet,
|
||||
id,
|
||||
reminders: pet.reminders || [],
|
||||
logs: pet.logs || [],
|
||||
consumptions: pet.consumptions || []
|
||||
};
|
||||
set((state) => ({
|
||||
pets: [...state.pets, newPet],
|
||||
activePetId: state.activePetId || id,
|
||||
}));
|
||||
}
|
||||
},
|
||||
removePet: async (id) => {
|
||||
const { useUserStore } = await import("./userStore");
|
||||
const isLoggedIn = useUserStore.getState().isLoggedIn;
|
||||
|
||||
if (isLoggedIn) {
|
||||
try {
|
||||
await api.delete(`/pets/${id}`);
|
||||
await useUserStore.getState().fetchProfile();
|
||||
} catch (error) {
|
||||
console.error("Failed to remove pet via backend API:", error);
|
||||
toast.error("خطا در حذف پت از سرور");
|
||||
}
|
||||
} else {
|
||||
set((state) => {
|
||||
const newPets = state.pets.filter((p) => p.id !== id);
|
||||
return {
|
||||
pets: newPets,
|
||||
activePetId: state.activePetId === id ? (newPets[0]?.id || null) : state.activePetId,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
setActivePet: (id) => set({ activePetId: id }),
|
||||
getActivePet: () => {
|
||||
const { pets, activePetId } = get();
|
||||
return pets.find((p) => p.id === activePetId) || null;
|
||||
},
|
||||
updatePet: async (id, updates) => {
|
||||
const { useUserStore } = await import("./userStore");
|
||||
const isLoggedIn = useUserStore.getState().isLoggedIn;
|
||||
|
||||
if (isLoggedIn) {
|
||||
try {
|
||||
await api.patch(`/pets/${id}`, {
|
||||
name: updates.name,
|
||||
type: updates.type,
|
||||
breed: updates.breed,
|
||||
weight: updates.weight ? Number(updates.weight) : undefined
|
||||
});
|
||||
await useUserStore.getState().fetchProfile();
|
||||
} catch (error) {
|
||||
console.error("Failed to update pet via backend API:", error);
|
||||
toast.error("خطا در ویرایش اطلاعات پت در سرور");
|
||||
}
|
||||
} else {
|
||||
set((state) => ({
|
||||
pets: state.pets.map((p) => (p.id === id ? { ...p, ...updates } : p)),
|
||||
}));
|
||||
}
|
||||
},
|
||||
addReminder: async (petId, reminder) => {
|
||||
try {
|
||||
await api.post(`/pets/${petId}/reminders`, reminder);
|
||||
const { useUserStore } = await import("./userStore");
|
||||
await useUserStore.getState().fetchProfile();
|
||||
} catch (error) {
|
||||
console.error("Failed to add reminder:", error);
|
||||
toast.error("خطا در ثبت یادآور");
|
||||
}
|
||||
},
|
||||
toggleReminder: async (petId, reminderId, date) => {
|
||||
try {
|
||||
await api.post(`/pets/${petId}/reminders/${reminderId}/toggle`, { date });
|
||||
const { useUserStore } = await import("./userStore");
|
||||
await useUserStore.getState().fetchProfile();
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle reminder:", error);
|
||||
toast.error("خطا در تغییر وضعیت یادآور");
|
||||
}
|
||||
},
|
||||
addHealthLog: async (petId, log) => {
|
||||
try {
|
||||
await api.post(`/pets/${petId}/health-logs`, log);
|
||||
const { useUserStore } = await import("./userStore");
|
||||
await useUserStore.getState().fetchProfile();
|
||||
} catch (error) {
|
||||
console.error("Failed to add health log:", error);
|
||||
toast.error("خطا در ثبت گزارش سلامتی");
|
||||
}
|
||||
},
|
||||
setPets: (backendPets) => {
|
||||
const mappedPets: PetProfile[] = backendPets.map(bp => ({
|
||||
id: bp.id,
|
||||
name: bp.name,
|
||||
type: bp.type as any,
|
||||
breed: bp.breed,
|
||||
age: Number(bp.age),
|
||||
weight: Number(bp.weight),
|
||||
activityLevel: bp.activityLevel as any,
|
||||
medicalConditions: bp.medicalConditions?.map((mc: any) => mc.condition) || [],
|
||||
image: bp.imageUrl || undefined,
|
||||
reminders: bp.reminders?.map((r: any) => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
time: r.time,
|
||||
frequency: r.frequency as any,
|
||||
productId: r.productId,
|
||||
completedDates: r.completions?.map((c: any) => c.completedDate) || []
|
||||
})) || [],
|
||||
logs: bp.healthLogs?.map((hl: any) => ({
|
||||
id: hl.id,
|
||||
date: hl.loggedDate,
|
||||
appetite: hl.appetite as any,
|
||||
energy: hl.energy as any,
|
||||
digestion: hl.digestion as any,
|
||||
note: hl.note || undefined
|
||||
})) || [],
|
||||
consumptions: []
|
||||
}));
|
||||
set({ pets: mappedPets, activePetId: get().activePetId && mappedPets.some(p => p.id === get().activePetId) ? get().activePetId : (mappedPets[0]?.id || null) });
|
||||
},
|
||||
}),
|
||||
{ name: "canina-pets" }
|
||||
)
|
||||
);
|
||||
196
frontend/application/lib/store/userStore.ts
Normal file
196
frontend/application/lib/store/userStore.ts
Normal file
@ -0,0 +1,196 @@
|
||||
"use client";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { authService } from "../services/authService";
|
||||
import api from "../services/api";
|
||||
import { useCartStore } from "./cartStore";
|
||||
import { usePetStore } from "./usePetStore";
|
||||
|
||||
|
||||
|
||||
export type UserRole = "User_Guest" | "User_PetOwner" | "User_Partner";
|
||||
|
||||
export interface Address {
|
||||
id: string;
|
||||
title: string;
|
||||
receptorName: string;
|
||||
phone: string;
|
||||
province: string;
|
||||
city: string;
|
||||
detail: string;
|
||||
zipCode: string;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
type: 'top_up' | 'purchase';
|
||||
amount: number;
|
||||
date: string;
|
||||
status: 'success' | 'failed' | 'pending';
|
||||
}
|
||||
|
||||
interface UserProfile {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
mobile: string;
|
||||
walletBalance: number;
|
||||
charityDonationTotal: number;
|
||||
addresses: Address[];
|
||||
transactions: Transaction[];
|
||||
}
|
||||
|
||||
interface UserStore {
|
||||
role: UserRole;
|
||||
isLoggedIn: boolean;
|
||||
profile: UserProfile;
|
||||
setRole: (role: UserRole) => void;
|
||||
setLoggedIn: (isLoggedIn: boolean) => void;
|
||||
updateProfile: (profile: Partial<UserProfile>) => Promise<void>;
|
||||
addAddress: (address: Address) => Promise<void>;
|
||||
updateAddress: (id: string, address: Address) => Promise<void>;
|
||||
deleteAddress: (id: string) => Promise<void>;
|
||||
setDefaultAddress: (id: string) => Promise<void>;
|
||||
topUpWallet: (amount: number) => Promise<void>;
|
||||
logout: () => void;
|
||||
fetchProfile: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useUserStore = create<UserStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
role: "User_Guest",
|
||||
isLoggedIn: false,
|
||||
profile: {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
mobile: "",
|
||||
walletBalance: 0,
|
||||
charityDonationTotal: 0,
|
||||
addresses: [],
|
||||
transactions: [],
|
||||
},
|
||||
setRole: (role) => set({ role }),
|
||||
setLoggedIn: (isLoggedIn) => set({ isLoggedIn }),
|
||||
updateProfile: async (updates) => {
|
||||
try {
|
||||
const profileData = await authService.updateProfile(updates);
|
||||
set((state) => ({
|
||||
profile: {
|
||||
...state.profile,
|
||||
firstName: profileData.firstName,
|
||||
lastName: profileData.lastName,
|
||||
email: profileData.email,
|
||||
mobile: profileData.mobile || "",
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Failed to update profile:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
addAddress: async (address) => {
|
||||
try {
|
||||
// Remove ID so backend generates UUID
|
||||
const { id, ...addressData } = address;
|
||||
await api.post('/users/addresses', addressData);
|
||||
await useUserStore.getState().fetchProfile();
|
||||
} catch (error) {
|
||||
console.error("Failed to add address:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
updateAddress: async (id, updated) => {
|
||||
try {
|
||||
const { id: _, ...addressData } = updated;
|
||||
await api.patch(`/users/addresses/${id}`, addressData);
|
||||
await useUserStore.getState().fetchProfile();
|
||||
} catch (error) {
|
||||
console.error("Failed to update address:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
deleteAddress: async (id) => {
|
||||
try {
|
||||
await api.delete(`/users/addresses/${id}`);
|
||||
await useUserStore.getState().fetchProfile();
|
||||
} catch (error) {
|
||||
console.error("Failed to delete address:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
setDefaultAddress: async (id) => {
|
||||
try {
|
||||
await api.patch(`/users/addresses/${id}/default`);
|
||||
await useUserStore.getState().fetchProfile();
|
||||
} catch (error) {
|
||||
console.error("Failed to set default address:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
topUpWallet: async (amount) => {
|
||||
try {
|
||||
await api.post('/users/wallet/top-up', { amount });
|
||||
await useUserStore.getState().fetchProfile();
|
||||
} catch (error) {
|
||||
console.error("Failed to top-up wallet:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
logout: () => {
|
||||
authService.logout();
|
||||
set({ role: "User_Guest", isLoggedIn: false, profile: {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
mobile: "",
|
||||
walletBalance: 0,
|
||||
charityDonationTotal: 0,
|
||||
addresses: [],
|
||||
transactions: []
|
||||
}});
|
||||
},
|
||||
fetchProfile: async () => {
|
||||
try {
|
||||
const profileData = await authService.getProfile();
|
||||
set({
|
||||
isLoggedIn: true,
|
||||
role: (profileData.role as UserRole) || "User_PetOwner",
|
||||
profile: {
|
||||
firstName: profileData.firstName,
|
||||
lastName: profileData.lastName,
|
||||
email: profileData.email,
|
||||
mobile: profileData.mobile || "",
|
||||
walletBalance: Number(profileData.walletBalance),
|
||||
charityDonationTotal: Number(profileData.charityDonationTotal),
|
||||
addresses: (profileData as any).addresses || [],
|
||||
transactions: (profileData as any).walletTransactions?.map((t: any) => ({
|
||||
id: t.id,
|
||||
type: t.type === 'deposit' ? 'top_up' : 'purchase',
|
||||
amount: Number(t.amount),
|
||||
date: t.createdAt,
|
||||
status: t.status === 'completed' ? 'success' : t.status === 'failed' ? 'failed' : 'pending'
|
||||
})) || []
|
||||
}
|
||||
});
|
||||
|
||||
// Sync orders list to CartStore
|
||||
if (profileData.orders) {
|
||||
useCartStore.getState().setOrders(profileData.orders);
|
||||
}
|
||||
// Sync pets list to PetStore
|
||||
if (profileData.pets) {
|
||||
usePetStore.getState().setPets(profileData.pets);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user profile:", error);
|
||||
authService.logout();
|
||||
set({ isLoggedIn: false, role: "User_Guest" });
|
||||
}
|
||||
},
|
||||
}),
|
||||
{ name: "canina-user" }
|
||||
)
|
||||
);
|
||||
8
frontend/application/lib/utils/utils.ts
Normal file
8
frontend/application/lib/utils/utils.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export const toPersian = (n: number | string | undefined | null) => {
|
||||
if (n === undefined || n === null) return "";
|
||||
return n.toString().replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[parseInt(d)]);
|
||||
};
|
||||
|
||||
export function cn(...classes: (string | boolean | undefined)[]) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
Loading…
Reference in New Issue
Block a user