feat: initialize project structure
- Configure project metadata and dependencies - Setup Vite with React and TypeScript - Add environment configuration and .gitignore - Implement initial application entry point and assets
This commit is contained in:
parent
c24867d90c
commit
c95c9e5ce0
9
.env.example
Normal file
9
.env.example
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
||||||
|
# AI Studio automatically injects this at runtime from user secrets.
|
||||||
|
# Users configure this via the Secrets panel in the AI Studio UI.
|
||||||
|
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||||
|
|
||||||
|
# APP_URL: The URL where this applet is hosted.
|
||||||
|
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
||||||
|
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
||||||
|
APP_URL="MY_APP_URL"
|
||||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
36
BACKEND_INTEGRATION.md
Normal file
36
BACKEND_INTEGRATION.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Backend Integration Map | نقشه اتصال به بکبند 🏗️
|
||||||
|
|
||||||
|
این سند شامل تمامی نقاط اتصال (End-points) مورد نیاز برای عملیاتی کردن پلتفرم کانینا ایران است.
|
||||||
|
|
||||||
|
## لیست TODOهای تزریق شده در کد:
|
||||||
|
|
||||||
|
### ۱. سرویس محصولات (Product Service)
|
||||||
|
- **GET /api/products**: دریافت لیست محصولات با فیلتر نژاد، دستهبندی و جستجو.
|
||||||
|
|
||||||
|
### ۲. مدیریت کاربران (User Store)
|
||||||
|
- **POST /api/auth/login**: ورود با موبایل یا تفکیک نقش (صاحب پت/همکار).
|
||||||
|
- **POST /api/auth/logout**: خروج از حساب کاربری.
|
||||||
|
- **PATCH /api/user/profile**: بروزرسانی اطلاعات پروفایل.
|
||||||
|
- **POST /api/user/addresses**: ثبت آدرس جدید.
|
||||||
|
- **PATCH /api/user/addresses/{id}**: ویرایش آدرس موجود.
|
||||||
|
- **DELETE /api/user/addresses/{id}**: حذف آدرس.
|
||||||
|
- **POST /api/user/addresses/{id}/set-default**: انتخاب آدرس پیشفرض.
|
||||||
|
- **POST /api/user/wallet/top-up**: شارژ کیف پول.
|
||||||
|
|
||||||
|
### ۳. پرونده سلامت پت (Pet Store)
|
||||||
|
- **POST /api/pets**: ثبت پت جدید (صدور شناسنامه).
|
||||||
|
- **PATCH /api/pets/{id}**: ویرایش اطلاعات پت.
|
||||||
|
- **DELETE /api/pets/{id}**: حذف پروفایل پت.
|
||||||
|
- **POST /api/pets/{petId}/reminders**: افزودن یادآور جدید.
|
||||||
|
- **POST /api/pets/{petId}/reminders/{reminderId}/toggle**: ثبت انجام یادآور (کاهش موجودی انبار شخصی).
|
||||||
|
- **POST /api/pets/{petId}/health-logs**: ثبت گزارش سلامت روزانه.
|
||||||
|
|
||||||
|
### ۴. سبد خرید و پرداخت (Cart & Payment)
|
||||||
|
- **POST /api/cart/apply-coupon**: استعلام و اعمال کد تخفیف.
|
||||||
|
- **POST /api/orders**: ثبت نهایی سفارش و تولید کد رهگیری.
|
||||||
|
- **POST /api/payment/init**: اتصال به درگاه پرداخت بانکی.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## راهنمای پیادهسازی:
|
||||||
|
تمامی این نقاط در کد با کامنت `// TODO: [BACKEND_API]` مشخص شدهاند. کافیست در هر بخش، منطق `set` یا `fetch` فعلی را با یک فراخوانی `axios` یا `fetch` جایگزین کنید.
|
||||||
50
README.md
50
README.md
@ -1,11 +1,49 @@
|
|||||||
<div align="center">
|
# Canina Iran | کانینا ایران 🐾
|
||||||
|
|
||||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
پلتفرم تخصصی مکملهای فوقدارویی حیوانات خانگی (نمایندگی انحصاری Canina pharma GmbH آلمان).
|
||||||
|
|
||||||
<h1>Built with AI Studio</h2>
|
## 🌟 قابلیتهای کلیدی (Key Features)
|
||||||
|
|
||||||
<p>The fastest path from prompt to production with Gemini.</p>
|
- **Smart Advisor (مشاور هوشمند)**: سیستم تحلیل بالینی مبتنی بر وضعیت سلامت، نژاد و سن پت برای پیشنهاد دقیق مکمل.
|
||||||
|
- **Digital Health ID (شناسنامه سلامت)**: مدیریت کامل سوابق درمانی، یادآورها و موجودی انبار شخصی برای هر حیوان.
|
||||||
|
- **Kindness Footprint (ردپای مهربانی)**: سیستم خیریه یکپارچه برای حمایت از حیوانات بیسرپرست پناهگاهی با هر خرید.
|
||||||
|
- **PWA Support**: قابلیت نصب بر روی موبایل و پشتیبانی از حالت آفلاین برای مشاهده شناسنامهها.
|
||||||
|
- **B2B Portal**: پورتال اختصاصی برای داروخانهها و کلینیکهای دامپزشکی.
|
||||||
|
|
||||||
<a href="https://aistudio.google.com/apps">Start building</a>
|
## 🛠 تکنولوژیها (Tech Stack)
|
||||||
|
|
||||||
</div>
|
- **Frontend**: React 18, Vite, TypeScript
|
||||||
|
- **Styling**: Tailwind CSS (Medical Design System)
|
||||||
|
- **State Management**: Zustand
|
||||||
|
- **Animations**: Framer Motion
|
||||||
|
- **Icons**: Lucide React
|
||||||
|
- **Notifications**: Sonner
|
||||||
|
|
||||||
|
## 🚀 نصب و اجرا (Installation)
|
||||||
|
|
||||||
|
۱. شبیهسازی مخزن:
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
```
|
||||||
|
|
||||||
|
۲. نصب وابستگیها:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
۳. اجرای پروژه در محیط توسعه:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📐 معماری پروژه (Architecture)
|
||||||
|
|
||||||
|
پروژه از معماری **Component-Based** با جداسازی لایههای داده (Services) و مدیریت وضعیت (Stores) استفاده میکند:
|
||||||
|
- `src/services/productService.ts`: مدیریت دریافت و فیلترینگ هوشمند محصولات.
|
||||||
|
- `src/store/usePetStore.ts`: مدیریت متمرکز پروفایلها و محاسبات سلامت پت.
|
||||||
|
- `src/components/SmartAdvisor.tsx`: موتور تحلیل بالینی کاربرمحور.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**توسعه یافته با عشق برای حیوانات ایران** ❤️
|
||||||
|
**Canina Pharma Laboratory - Proudly German Quality**
|
||||||
|
|||||||
23
index.html
Normal file
23
index.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#005EB8" />
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<link rel="apple-touch-icon" href="https://upload.wikimedia.org/wikipedia/commons/d/d5/Paw_Print.svg" />
|
||||||
|
<title>Canina - مراقبت هوشمند از پت</title>
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/sw.js').catch(ev => console.log('SW reg failed', ev));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
8
metadata.json
Normal file
8
metadata.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "Canina Iran - Official Representative",
|
||||||
|
"description": "The official representative of Canina pharma GmbH in Iran. Specialist in veterinary supplements and pharmaceutical-grade pet care products since 1984.",
|
||||||
|
"requestFramePermissions": [],
|
||||||
|
"majorCapabilities": [
|
||||||
|
"MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"
|
||||||
|
]
|
||||||
|
}
|
||||||
4351
package-lock.json
generated
Normal file
4351
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
package.json
Normal file
39
package.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "react-example",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"clean": "rm -rf dist server.js",
|
||||||
|
"lint": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@google/genai": "^1.29.0",
|
||||||
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"lucide-react": "^0.546.0",
|
||||||
|
"motion": "^12.23.24",
|
||||||
|
"react": "^19.0.1",
|
||||||
|
"react-dom": "^19.0.1",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
|
"vite": "^6.2.3",
|
||||||
|
"zustand": "^5.0.13"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/node": "^22.14.0",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "~5.8.2",
|
||||||
|
"vite": "^6.2.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
public/locales/fa/common.json
Normal file
20
public/locales/fa/common.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"api": {
|
||||||
|
"errors": {
|
||||||
|
"400": "درخواست شما نامعتبر است. لطفاً دادهها را بررسی کنید.",
|
||||||
|
"401": "دسترسی غیرمجاز. لطفا دوباره وارد شوید.",
|
||||||
|
"403": "شما اجازه دسترسی به این بخش را ندارید.",
|
||||||
|
"404": "سرویس یا منبع مورد نظر یافت نشد.",
|
||||||
|
"500": "خطای داخلی سرور. تیم فنی در حال بررسی است.",
|
||||||
|
"insufficient_balance": "موجودی کیف پول شما کافی نیست. لطفا حساب خود را شارژ کنید.",
|
||||||
|
"breed_mismatch": "این محصول با نژاد انتخاب شده پت شما سازگار نیست.",
|
||||||
|
"invalid_coupon": "کد تخفیف وارد شده معتبر نیست یا منقضی شده است.",
|
||||||
|
"order_failed": "متاسفانه ثبت سفارش با خطا مواجه شد. لطفا دوباره تلاش کنید."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"loading": "در حال دریافت اطلاعات از سرور...",
|
||||||
|
"saving": "در حال ذخیرهسازی تغییرات...",
|
||||||
|
"success_update": "تغییرات با موفقیت ذخیره شد."
|
||||||
|
}
|
||||||
|
}
|
||||||
21
public/manifest.json
Normal file
21
public/manifest.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "کانینا ایران | Canina Iran",
|
||||||
|
"short_name": "Canina",
|
||||||
|
"description": "مکملهای تخصصی حیوانات خانگی آلمان",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#005EB8",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "https://upload.wikimedia.org/wikipedia/commons/d/d5/Paw_Print.svg",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/svg+xml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "https://upload.wikimedia.org/wikipedia/commons/d/d5/Paw_Print.svg",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/svg+xml"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
23
public/sw.js
Normal file
23
public/sw.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const CACHE_NAME = 'canina-v1';
|
||||||
|
const ASSETS_TO_CACHE = [
|
||||||
|
'/',
|
||||||
|
'/index.html',
|
||||||
|
'/src/main.tsx',
|
||||||
|
'/src/index.css'
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
return cache.addAll(ASSETS_TO_CACHE);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then((response) => {
|
||||||
|
return response || fetch(event.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
419
src/App.tsx
Normal file
419
src/App.tsx
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, lazy, Suspense } from "react";
|
||||||
|
import Header from "./components/Header";
|
||||||
|
import Hero from "./components/Hero";
|
||||||
|
import Footer from "./components/Footer";
|
||||||
|
import { Toaster } from "sonner";
|
||||||
|
import { Product } from "./data/products";
|
||||||
|
import { useCartStore } from "./store/cartStore";
|
||||||
|
import { usePetStore } from "./store/usePetStore";
|
||||||
|
import { NetworkBanner } from './components/NetworkBanner';
|
||||||
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||||
|
import { NotFoundPage } from './components/ErrorPages';
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import labImage from "./assets/images/regenerated_image_1779109861747.png";
|
||||||
|
|
||||||
|
// Lazy Loaded Components
|
||||||
|
const ArchivePage = lazy(() => import("./components/ArchivePage"));
|
||||||
|
const ProductPage = lazy(() => import("./components/ProductPage"));
|
||||||
|
const PetProfile = lazy(() => import("./components/PetProfile"));
|
||||||
|
const UserDashboard = lazy(() => import("./components/UserDashboard"));
|
||||||
|
const SearchResultsPage = lazy(() => import("./components/SearchResultsPage"));
|
||||||
|
const IngredientWiki = lazy(() => import("./components/IngredientWiki"));
|
||||||
|
const BlogPage = lazy(() => import("./components/BlogPage"));
|
||||||
|
const VideosPage = lazy(() => import("./components/VideosPage"));
|
||||||
|
const CheckoutPage = lazy(() => import("./components/CheckoutPage"));
|
||||||
|
const OrderSuccess = lazy(() => import("./components/OrderSuccess"));
|
||||||
|
const OrderTracking = lazy(() => import("./components/OrderTracking"));
|
||||||
|
const SmartAdvisor = lazy(() => import("./components/SmartAdvisor"));
|
||||||
|
const LoginModal = lazy(() => import("./components/LoginModal"));
|
||||||
|
const CartDrawer = lazy(() => import("./components/CartDrawer"));
|
||||||
|
const B2BPortal = lazy(() => import("./components/B2BPortal"));
|
||||||
|
const FeaturedProducts = lazy(() => import("./components/FeaturedProducts"));
|
||||||
|
const VetGallery = lazy(() => import("./components/VetGallery"));
|
||||||
|
|
||||||
|
type View = "home" | "shop" | "wiki" | "product-detail" | "profile" | "checkout" | "search" | "order-success" | "order-tracking" | "blog" | "videos" | "user-dashboard";
|
||||||
|
|
||||||
|
function LoadingFallback() {
|
||||||
|
return (
|
||||||
|
<div className="h-[60vh] flex flex-col items-center justify-center bg-medical-gray-50/50 backdrop-blur-sm">
|
||||||
|
<div className="w-16 h-16 border-4 border-canina-blue/10 border-t-canina-blue rounded-full animate-spin mb-4" />
|
||||||
|
<span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest animate-pulse font-vazir">در حال آمادهسازی لابراتوار...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [currentView, setCurrentView] = useState<View>("home");
|
||||||
|
const [subView, setSubView] = useState<any>(null);
|
||||||
|
const [advisorRecommendation, setAdvisorRecommendation] = useState<string | null>(null);
|
||||||
|
const [advisorData, setAdvisorData] = useState<any>(null);
|
||||||
|
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||||
|
const [lastOrderId, setLastOrderId] = useState<string | null>(null);
|
||||||
|
const [initialCategory, setInitialCategory] = useState("all");
|
||||||
|
const [initialSearch, setInitialSearch] = useState("");
|
||||||
|
const [isCartOpen, setIsCartOpen] = useState(false);
|
||||||
|
const [showB2B, setShowB2B] = useState(false);
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const init = async () => {
|
||||||
|
// Simulating initial data load
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 800));
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
init();
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePopState = (event: PopStateEvent) => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const category = params.get("category") || "all";
|
||||||
|
const search = params.get("search") || "";
|
||||||
|
|
||||||
|
setInitialCategory(category);
|
||||||
|
setInitialSearch(search);
|
||||||
|
setCurrentView("shop");
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("popstate", handlePopState);
|
||||||
|
return () => window.removeEventListener("popstate", handlePopState);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { clearCart, addOrder, getTotal, items } = useCartStore();
|
||||||
|
const { getActivePet, pets } = usePetStore();
|
||||||
|
|
||||||
|
// Global Scroll To Top on view change
|
||||||
|
useEffect(() => {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}, [currentView]);
|
||||||
|
|
||||||
|
const handleProductClick = (product: Product) => {
|
||||||
|
setSelectedProduct(product);
|
||||||
|
setCurrentView("product-detail");
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToShop = (category: string = "all", search: string = "") => {
|
||||||
|
// Update URL with search params
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (category && category !== "all") params.set("category", category);
|
||||||
|
if (search) params.set("search", search);
|
||||||
|
|
||||||
|
const newUrl = params.toString() ? `?${params.toString()}` : "/shop";
|
||||||
|
window.history.pushState({ category, search }, "", newUrl);
|
||||||
|
|
||||||
|
setInitialCategory(category);
|
||||||
|
setInitialSearch(search);
|
||||||
|
setCurrentView("shop");
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToWiki = () => {
|
||||||
|
setCurrentView("wiki");
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNavigate = (v: any) => {
|
||||||
|
if (typeof v === 'object') {
|
||||||
|
setCurrentView(v.view);
|
||||||
|
setSubView(v.subview || null);
|
||||||
|
} else {
|
||||||
|
setCurrentView(v);
|
||||||
|
setSubView(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToProfile = (subview: any = "index") => {
|
||||||
|
setCurrentView("profile");
|
||||||
|
setSubView(subview);
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToDashboard = () => {
|
||||||
|
setCurrentView("user-dashboard");
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckout = () => {
|
||||||
|
setIsCartOpen(false);
|
||||||
|
setCurrentView("checkout");
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (q: string) => {
|
||||||
|
setSearchQuery(q);
|
||||||
|
setCurrentView("search");
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalizeOrder = () => {
|
||||||
|
const activePet = getActivePet();
|
||||||
|
const orderId = addOrder({
|
||||||
|
items,
|
||||||
|
total: getTotal(),
|
||||||
|
petId: activePet?.id
|
||||||
|
});
|
||||||
|
setLastOrderId(orderId);
|
||||||
|
clearCart();
|
||||||
|
setCurrentView("order-success");
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdvisorComplete = (data: any) => {
|
||||||
|
setAdvisorData(data);
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
setShowLoginModal(true);
|
||||||
|
} else {
|
||||||
|
processAdvisorResult(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processAdvisorResult = (data?: any) => {
|
||||||
|
const d = data || advisorData;
|
||||||
|
if (!d) return;
|
||||||
|
|
||||||
|
// Map medical conditions to a broad recommendation category for the suggested items logic in PetProfile
|
||||||
|
if (d.medicalConditions.includes("جراحی مفاصل")) setAdvisorRecommendation("joints");
|
||||||
|
else if (d.medicalConditions.includes("مشکلات گوارشی")) setAdvisorRecommendation("digestion");
|
||||||
|
else if (d.medicalConditions.includes("ریزش موی شدید")) setAdvisorRecommendation("skin");
|
||||||
|
else if (d.medicalConditions.includes("بیاشتهایی") || d.medicalConditions.includes("زایمان اخیر") || d.medicalConditions.includes("بارداری")) setAdvisorRecommendation("vitamins");
|
||||||
|
|
||||||
|
// Clear recommended items and add the pet to the store
|
||||||
|
const { addPet } = usePetStore.getState();
|
||||||
|
addPet(d);
|
||||||
|
|
||||||
|
toast.success(`تحلیل سلامت ${d.name} آماده است! شناسنامه صادر شد.`);
|
||||||
|
navigateToProfile("detail");
|
||||||
|
setAdvisorData(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = () => {
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
setShowLoginModal(false);
|
||||||
|
|
||||||
|
if (advisorData) {
|
||||||
|
toast.success(`خوش آمدید! مشخصات ${advisorData.name} در شناسنامه ثبت شد.`);
|
||||||
|
processAdvisorResult();
|
||||||
|
} else {
|
||||||
|
toast.success("خوش آمدید!");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderView = () => {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex flex-col items-center justify-center bg-medical-gray-50">
|
||||||
|
<div className="w-20 h-20 border-4 border-canina-blue/20 border-t-canina-blue rounded-full animate-spin mb-6" />
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-10 h-10 bg-canina-blue rounded-2xl flex items-center justify-center text-white font-bold text-xl shadow-xl shadow-canina-blue/20 italic">C</div>
|
||||||
|
<span className="text-[12px] font-black text-medical-gray-900 uppercase tracking-widest animate-pulse">Canina Pharma Laboratory</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<LoadingFallback />}>
|
||||||
|
{(() => {
|
||||||
|
switch (currentView) {
|
||||||
|
case "user-dashboard":
|
||||||
|
return <UserDashboard onBack={() => setCurrentView("home")} onNavigate={handleNavigate} />;
|
||||||
|
case "search":
|
||||||
|
return (
|
||||||
|
<SearchResultsPage
|
||||||
|
query={searchQuery}
|
||||||
|
onProductClick={handleProductClick}
|
||||||
|
onBack={() => setCurrentView("home")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "checkout":
|
||||||
|
return <CheckoutPage onBack={() => setIsCartOpen(true)} onComplete={finalizeOrder} />;
|
||||||
|
case "product-detail":
|
||||||
|
return selectedProduct ? (
|
||||||
|
<ProductPage
|
||||||
|
product={selectedProduct}
|
||||||
|
onBack={() => setCurrentView("shop")}
|
||||||
|
onProductClick={handleProductClick}
|
||||||
|
onWikiNavigate={() => setCurrentView("wiki")}
|
||||||
|
onShopNavigate={navigateToShop}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
case "shop":
|
||||||
|
return <ArchivePage onProductClick={handleProductClick} initialCategory={initialCategory} initialSearch={initialSearch} onBack={() => setCurrentView("home")} onShopNavigate={navigateToShop} />;
|
||||||
|
case "home":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Hero onProfileClick={() => navigateToProfile("detail")} onShopNavigate={() => navigateToShop()} />
|
||||||
|
<SmartAdvisor onComplete={handleAdvisorComplete} />
|
||||||
|
<FeaturedProducts onProductClick={handleProductClick} onShopNavigate={() => navigateToShop()} />
|
||||||
|
<VetGallery onNavigate={handleNavigate} />
|
||||||
|
|
||||||
|
{/* Navigation to Sections */}
|
||||||
|
<section className="py-20 bg-medical-gray-50 border-y border-medical-gray-100">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 grid md:grid-cols-2 gap-8">
|
||||||
|
<div
|
||||||
|
onClick={() => navigateToShop()}
|
||||||
|
className="bg-white p-12 rounded-[3.5rem] border border-medical-gray-200 shadow-sm hover:shadow-2xl hover:-translate-y-2 transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<h3 className="text-3xl font-black text-medical-gray-900 mb-4 group-hover:text-canina-blue transition-colors italic">فروشگاه تخصصی</h3>
|
||||||
|
<p className="text-medical-gray-500 mb-8 leading-relaxed">دسترسی به تمامی محصولات بر اساس علائم بالینی و نیازهای پت شما.</p>
|
||||||
|
<div className="flex items-center gap-2 text-canina-blue font-black tracking-widest uppercase text-xs">
|
||||||
|
کاتالوگ کامل محصولات
|
||||||
|
<span className="text-lg">←</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={navigateToWiki}
|
||||||
|
className="bg-medical-gray-900 p-12 rounded-[3.5rem] text-white hover:shadow-2xl hover:-translate-y-2 transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<h3 className="text-3xl font-black mb-4 group-hover:text-canina-blue transition-colors italic">دانشنامه علمی</h3>
|
||||||
|
<p className="text-white/60 mb-8 leading-relaxed">آشنایی با ترکیبات نایاب و استانداردهای دارویی محصولات آلمانی کانینا.</p>
|
||||||
|
<div className="flex items-center gap-2 text-canina-blue font-black tracking-widest uppercase text-xs">
|
||||||
|
مطالعه مقالات تخصصی
|
||||||
|
<span className="text-lg">←</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* About Section Teaser */}
|
||||||
|
<section className="py-24 bg-white overflow-hidden">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 grid lg:grid-cols-2 gap-16 items-center">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute -top-10 -right-10 w-40 h-40 bg-canina-blue/5 rounded-full blur-3xl" />
|
||||||
|
<img
|
||||||
|
src={labImage}
|
||||||
|
alt="Laboratory"
|
||||||
|
className="rounded-[3rem] shadow-2xl relative z-10 grayscale-[10%]"
|
||||||
|
/>
|
||||||
|
<div className="absolute -bottom-6 -left-6 bg-canina-blue text-white p-8 rounded-3xl shadow-xl z-20">
|
||||||
|
<div className="text-4xl font-black mb-1 italic">کیفیت</div>
|
||||||
|
<div className="text-xs uppercase tracking-widest font-bold opacity-80">استانداردهای فوقدارویی</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-canina-blue text-xs font-bold uppercase tracking-[0.2em] mb-4">میراث ما از آلمان</div>
|
||||||
|
<h2 className="text-3xl lg:text-5xl font-black text-medical-gray-900 leading-[1.2] mb-8">
|
||||||
|
چرا برند آلمانی <span className="text-canina-blue italic">Canina</span> مرجع دامپزشکان است؟
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{[
|
||||||
|
{ title: "مواد اولیه نایاب", desc: "استفاده از پودر صدف لبسبز اصل نیوزیلند و مواد ارگانیک با گرید دارویی." },
|
||||||
|
{ title: "فاقد مواد نگهدارنده", desc: "تمامی محصولات ۱۰۰٪ طبیعی و فاقد رنگهای مصنوعی و طعمدهندههای شیمیایی هستند." },
|
||||||
|
{ title: "تاییدیه اروپا", desc: "مطابق با سختگیرانهترین استانداردهای ایمنی مواد غذایی و دارویی در اتحادیه اروپا." }
|
||||||
|
].map((item, idx) => (
|
||||||
|
<div key={idx} className="flex gap-4">
|
||||||
|
<div className="w-12 h-12 rounded-2xl bg-medical-gray-50 flex items-center justify-center flex-shrink-0 text-canina-blue font-bold">
|
||||||
|
{idx + 1}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-lg font-bold text-medical-gray-900 mb-1">{item.title}</h4>
|
||||||
|
<p className="text-sm text-medical-gray-500 leading-relaxed font-medium">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case "wiki":
|
||||||
|
return <IngredientWiki onProductClick={handleProductClick} onBack={() => setCurrentView("home")} />;
|
||||||
|
case "profile":
|
||||||
|
return <PetProfile onProductClick={handleProductClick} onBack={() => setCurrentView("home")} initialView={subView} advisorNeed={advisorRecommendation} />;
|
||||||
|
case "order-success":
|
||||||
|
return lastOrderId ? <OrderSuccess orderId={lastOrderId} onNavigate={handleNavigate} /> : null;
|
||||||
|
case "order-tracking":
|
||||||
|
return <OrderTracking onNavigate={handleNavigate} onBack={() => setCurrentView("home")} />;
|
||||||
|
case "blog":
|
||||||
|
return <BlogPage onBack={() => setCurrentView("home")} onProductClick={handleProductClick} />;
|
||||||
|
case "videos":
|
||||||
|
return <VideosPage onBack={() => setCurrentView("home")} />;
|
||||||
|
default:
|
||||||
|
return <NotFoundPage onGoHome={() => setCurrentView("home")} />;
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div className="min-h-screen bg-medical-gray-50 font-sans" dir="rtl">
|
||||||
|
<NetworkBanner />
|
||||||
|
<Header
|
||||||
|
onNavigate={handleNavigate}
|
||||||
|
onShopNavigate={navigateToShop}
|
||||||
|
currentView={currentView}
|
||||||
|
onCartOpen={() => setIsCartOpen(true)}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onB2BOpen={() => setShowB2B(true)}
|
||||||
|
/>
|
||||||
|
<main>
|
||||||
|
{renderView()}
|
||||||
|
|
||||||
|
{/* Call to Action */}
|
||||||
|
<section className="bg-canina-blue py-20 relative overflow-hidden">
|
||||||
|
<div className="absolute inset-0 opacity-10">
|
||||||
|
<div className="grid grid-cols-10 h-full w-full">
|
||||||
|
{Array.from({length: 100}).map((_, i) => (
|
||||||
|
<div key={i} className="border border-white/20" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-4xl mx-auto px-4 text-center relative z-10">
|
||||||
|
<h2 className="text-3xl lg:text-5xl font-black text-white mb-8 leading-tight">
|
||||||
|
میخواهید بدانید کدام محصول برای پت شما مناسبتر است؟
|
||||||
|
</h2>
|
||||||
|
<p className="text-white/80 text-lg mb-10 leading-relaxed">
|
||||||
|
تیم متخصص دامپزشکی کانینا ایران آماده پاسخگویی به سوالات شماست.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
|
<button className="bg-white text-canina-blue px-12 py-5 rounded-full font-black text-xl hover:scale-105 transition-transform shadow-2xl shadow-black/20">
|
||||||
|
دریافت رژیم مکمل رایگان
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => navigateToShop()}
|
||||||
|
className="bg-canina-blue/20 backdrop-blur-md border-2 border-white text-white px-12 py-5 rounded-full font-black text-xl hover:bg-white hover:text-canina-blue transition-all"
|
||||||
|
>
|
||||||
|
ورود به محصولات تخصصی
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer onNavigate={handleNavigate} onShopNavigate={navigateToShop} onB2BOpen={() => setShowB2B(true)} />
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
{showB2B && <B2BPortal onClose={() => setShowB2B(false)} />}
|
||||||
|
<CartDrawer
|
||||||
|
isOpen={isCartOpen}
|
||||||
|
onClose={() => setIsCartOpen(false)}
|
||||||
|
onCheckout={handleCheckout}
|
||||||
|
onShopNavigate={navigateToShop}
|
||||||
|
/>
|
||||||
|
<LoginModal
|
||||||
|
isOpen={showLoginModal}
|
||||||
|
onClose={() => {
|
||||||
|
setShowLoginModal(false);
|
||||||
|
setAdvisorData(null); // Clear advisor data if closed without login
|
||||||
|
}}
|
||||||
|
onLogin={handleLogin}
|
||||||
|
petName={advisorData?.name}
|
||||||
|
isAdvisorContext={!!advisorData}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
<Toaster position="top-center" expand={true} richColors />
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
src/assets/images/regenerated_image_1779109861747.png
Normal file
BIN
src/assets/images/regenerated_image_1779109861747.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
289
src/components/AddressModal.tsx
Normal file
289
src/components/AddressModal.tsx
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { X, MapPin, User, Phone, Hash, Save, Check } from "lucide-react";
|
||||||
|
import { toPersian, cn } from "../lib/utils";
|
||||||
|
import { Address } from "../store/userStore";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface AddressModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (address: Address) => void;
|
||||||
|
editingAddress?: Address | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddressModal({ isOpen, onClose, onSave, editingAddress }: AddressModalProps) {
|
||||||
|
const [formData, setFormData] = useState<Omit<Address, "id">>({
|
||||||
|
title: "",
|
||||||
|
receptorName: "",
|
||||||
|
phone: "",
|
||||||
|
province: "",
|
||||||
|
city: "",
|
||||||
|
detail: "",
|
||||||
|
zipCode: "",
|
||||||
|
isDefault: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingAddress) {
|
||||||
|
setFormData({
|
||||||
|
title: editingAddress.title,
|
||||||
|
receptorName: editingAddress.receptorName,
|
||||||
|
phone: editingAddress.phone,
|
||||||
|
province: editingAddress.province,
|
||||||
|
city: editingAddress.city,
|
||||||
|
detail: editingAddress.detail,
|
||||||
|
zipCode: editingAddress.zipCode,
|
||||||
|
isDefault: editingAddress.isDefault
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
title: "",
|
||||||
|
receptorName: "",
|
||||||
|
phone: "",
|
||||||
|
province: "",
|
||||||
|
city: "",
|
||||||
|
detail: "",
|
||||||
|
zipCode: "",
|
||||||
|
isDefault: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setErrors({});
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}, [editingAddress, isOpen]);
|
||||||
|
|
||||||
|
const normalizeDigits = (str: string) => {
|
||||||
|
const persianDigits = [/۰/g, /۱/g, /۲/g, /۳/g, /۴/g, /۵/g, /۶/g, /۷/g, /۸/g, /۹/g];
|
||||||
|
const arabicDigits = [/٠/g, /١/g, /٢/g, /٣/g, /٤/g, /٥/g, /٦/g, /٧/g, /٨/g, /٩/g];
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
str = str.replace(persianDigits[i], i.toString()).replace(arabicDigits[i], i.toString());
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
const newErrors: Record<string, string> = {};
|
||||||
|
if (!formData.title) newErrors.title = "لطفاً عنوان آدرس را وارد کنید";
|
||||||
|
if (!formData.receptorName) newErrors.receptorName = "نام گیرنده را وارد کنید";
|
||||||
|
|
||||||
|
const normalizedPhone = normalizeDigits(formData.phone);
|
||||||
|
if (!normalizedPhone) {
|
||||||
|
newErrors.phone = "شماره تماس الزامی است";
|
||||||
|
} else if (!/^09\d{9}$/.test(normalizedPhone)) {
|
||||||
|
newErrors.phone = "شماره موبایل وارد شده معتبر نیست";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formData.province) newErrors.province = "استان را مشخص کنید";
|
||||||
|
if (!formData.city) newErrors.city = "شهر را مشخص کنید";
|
||||||
|
if (!formData.detail) newErrors.detail = "آدرس دقیق پستی را وارد کنید";
|
||||||
|
|
||||||
|
const normalizedZip = normalizeDigits(formData.zipCode);
|
||||||
|
if (!normalizedZip) {
|
||||||
|
newErrors.zipCode = "کد پستی الزامی است";
|
||||||
|
} else if (!/^\d{10}$/.test(normalizedZip)) {
|
||||||
|
newErrors.zipCode = "کد پستی باید ۱۰ رقم باشد";
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return Object.keys(newErrors).length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (validate()) {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
// Simulate small delay for UX
|
||||||
|
setTimeout(() => {
|
||||||
|
onSave({
|
||||||
|
...formData,
|
||||||
|
phone: normalizeDigits(formData.phone),
|
||||||
|
zipCode: normalizeDigits(formData.zipCode),
|
||||||
|
id: editingAddress?.id || Math.random().toString(36).substr(2, 9),
|
||||||
|
isDefault: editingAddress?.isDefault || false
|
||||||
|
});
|
||||||
|
toast.success(editingAddress ? "تغییرات آدرس با موفقیت ذخیره شد" : "آدرس جدید با موفقیت اضافه شد");
|
||||||
|
setIsSubmitting(false);
|
||||||
|
onClose();
|
||||||
|
}, 600);
|
||||||
|
} else {
|
||||||
|
toast.error("لطفاً موارد خطای فرم را اصلاح کنید");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute inset-0 bg-medical-gray-900/60 backdrop-blur-md"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
className="bg-white w-full max-w-xl rounded-[3rem] relative z-10 shadow-2xl overflow-hidden font-vazir text-right pointer-events-auto"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<div className="bg-medical-gray-50 px-8 py-6 border-b border-medical-gray-100 flex items-center justify-between">
|
||||||
|
<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">
|
||||||
|
<MapPin className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-black text-medical-gray-900 italic">
|
||||||
|
{editingAddress ? "ویرایش آدرس ارسال" : "افزودن آدرس جدید"}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-white border border-medical-gray-200 text-medical-gray-400 hover:text-red-500 transition-all"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="p-8 space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Row 1: Title */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">عنوان آدرس (مثلاً: خانه، محل کار)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={e => setFormData({...formData, title: e.target.value})}
|
||||||
|
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20", errors.title ? "border-red-300" : "border-medical-gray-100")}
|
||||||
|
placeholder="مثال: منزل اصلی"
|
||||||
|
/>
|
||||||
|
{errors.title && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.title}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{/* Row 2: Name & Phone */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">نام گیرنده</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.receptorName}
|
||||||
|
onChange={e => setFormData({...formData, receptorName: e.target.value})}
|
||||||
|
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20", errors.receptorName ? "border-red-300" : "border-medical-gray-100")}
|
||||||
|
placeholder="نام و نامخانوادگی..."
|
||||||
|
/>
|
||||||
|
{errors.receptorName && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.receptorName}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">شماره تماس</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={e => setFormData({...formData, phone: e.target.value})}
|
||||||
|
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20 text-left", errors.phone ? "border-red-300" : "border-medical-gray-100")}
|
||||||
|
placeholder="09120000000"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
{errors.phone && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.phone}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{/* Row 3: Province & City */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">استان</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.province}
|
||||||
|
onChange={e => setFormData({...formData, province: e.target.value})}
|
||||||
|
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20", errors.province ? "border-red-300" : "border-medical-gray-100")}
|
||||||
|
placeholder="نام استان..."
|
||||||
|
/>
|
||||||
|
{errors.province && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.province}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">شهر</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.city}
|
||||||
|
onChange={e => setFormData({...formData, city: e.target.value})}
|
||||||
|
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20", errors.city ? "border-red-300" : "border-medical-gray-100")}
|
||||||
|
placeholder="نام شهر..."
|
||||||
|
/>
|
||||||
|
{errors.city && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.city}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 4: Detail Address */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">آدرس دقیق</label>
|
||||||
|
<textarea
|
||||||
|
rows={2}
|
||||||
|
value={formData.detail}
|
||||||
|
onChange={e => setFormData({...formData, detail: e.target.value})}
|
||||||
|
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20 resize-none", errors.detail ? "border-red-300" : "border-medical-gray-100")}
|
||||||
|
placeholder="خیابان، کوچه، پلاک، واحد..."
|
||||||
|
/>
|
||||||
|
{errors.detail && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.detail}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 5: Zip Code */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">کد پستی (۱۰ رقم)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formData.zipCode}
|
||||||
|
onChange={e => setFormData({...formData, zipCode: e.target.value})}
|
||||||
|
className={cn("w-full bg-medical-gray-50 border rounded-2xl py-3 px-5 outline-none font-bold transition-all focus:ring-2 focus:ring-canina-blue/20 text-left", errors.zipCode ? "border-red-300" : "border-medical-gray-100")}
|
||||||
|
placeholder="1234567890"
|
||||||
|
dir="ltr"
|
||||||
|
/>
|
||||||
|
{errors.zipCode && <p className="text-[10px] text-red-500 font-bold pr-2">{errors.zipCode}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 6: Default Toggle */}
|
||||||
|
<div className="flex items-center gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFormData({...formData, isDefault: !formData.isDefault})}
|
||||||
|
className={cn(
|
||||||
|
"w-6 h-6 rounded-lg border-2 flex items-center justify-center transition-all",
|
||||||
|
formData.isDefault ? "bg-canina-blue border-canina-blue text-white" : "border-medical-gray-200 text-transparent"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<label className="text-sm font-bold text-medical-gray-600">انتخاب به عنوان آدرس پیشفرض ارسال</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 py-4 bg-medical-gray-50 text-medical-gray-400 rounded-2xl font-black text-sm hover:bg-medical-gray-100 transition-all"
|
||||||
|
>
|
||||||
|
انصراف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="flex-[2] py-4 bg-canina-blue text-white rounded-2xl font-black text-sm flex items-center justify-center gap-3 hover:bg-medical-gray-900 transition-all shadow-lg shadow-canina-blue/20 disabled:opacity-50 disabled:cursor-wait"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
{isSubmitting ? "در حال ثبت..." : (editingAddress ? "ثبت تغییرات" : "ثبت آدرس نهایی")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
426
src/components/ArchivePage.tsx
Normal file
426
src/components/ArchivePage.tsx
Normal file
@ -0,0 +1,426 @@
|
|||||||
|
import React, { useState, useMemo, useEffect } from "react";
|
||||||
|
import { Product, PetType } from "../data/products";
|
||||||
|
import { usePetStore } from "../store/usePetStore";
|
||||||
|
import { productService } from "../services/productService";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { ProductCardSkeleton } from "./Skeleton";
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Filter,
|
||||||
|
Dog,
|
||||||
|
Cat,
|
||||||
|
ShieldCheck,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Activity,
|
||||||
|
Sparkles,
|
||||||
|
Stethoscope,
|
||||||
|
HeartPulse,
|
||||||
|
Heart,
|
||||||
|
AlertCircle,
|
||||||
|
Plus,
|
||||||
|
Loader2
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useCartStore } from "../store/cartStore";
|
||||||
|
import SafeImage from "./SafeImage";
|
||||||
|
|
||||||
|
const CATEGORIES = [
|
||||||
|
{ id: "all", label: "همه محصولات", icon: <Activity className="w-4 h-4" /> },
|
||||||
|
{ id: "joints", label: "حرکتی و مفاصل", icon: <HeartPulse className="w-4 h-4" /> },
|
||||||
|
{ id: "immune", label: "تقویت و ایمنی", icon: <ShieldCheck className="w-4 h-4" /> },
|
||||||
|
{ id: "energy", label: "ویتامین و انرژی", icon: <Activity className="w-4 h-4" /> },
|
||||||
|
{ id: "special-care", label: "پلتفرم مراقبت ویژه", icon: <Stethoscope className="w-4 h-4" /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CATEGORY_MAP: Record<string, { category?: string; query?: string; symptoms?: string[] }> = {
|
||||||
|
// Main Categories
|
||||||
|
"joints": { category: "joints" },
|
||||||
|
"immune": { category: "immune" },
|
||||||
|
"energy": { category: "energy" },
|
||||||
|
"special-care": { category: "special-care" },
|
||||||
|
|
||||||
|
// Persian Fallbacks (for robust matching)
|
||||||
|
"مفاصل و استخوان": { category: "joints" },
|
||||||
|
"تقویت سیستم ایمنی و گوارش": { category: "immune" },
|
||||||
|
"ویتامینها و انرژیبخشها": { category: "energy" },
|
||||||
|
"مراقبتهای ویژه (پوست، دندان و چشم)": { category: "special-care" },
|
||||||
|
|
||||||
|
// Solutions
|
||||||
|
"آرتروز سگهای پیر": { category: "joints", symptoms: ["درد مفاصل", "سختی در بلند شدن", "لنگیدن"] },
|
||||||
|
"رشد استخوانی تولهسگ": { category: "joints", symptoms: ["رشد سریع تولهسگ"] },
|
||||||
|
"تقویت رباط و تاندون": { category: "joints", query: "تاندون" },
|
||||||
|
|
||||||
|
"پیشگیری از بیماری": { category: "immune", symptoms: ["ضعف بعد از بیماری"] },
|
||||||
|
"رفع اسهال و یبوست": { category: "immune", symptoms: ["اسهال", "یبوست", "اسهال مزمن"] },
|
||||||
|
"پروبیوتیکها": { category: "immune", query: "فیبر" },
|
||||||
|
|
||||||
|
"مکملهای مولتیویتامین": { category: "energy", query: "ویتامین" },
|
||||||
|
"افزایش اشتها": { category: "energy", symptoms: ["بیاشتهایی"] },
|
||||||
|
"رشد و بلوغ": { category: "energy", query: "انرژی" },
|
||||||
|
|
||||||
|
"سلامت پوست و مفاصل": { category: "special-care", symptoms: ["خشکی پوست", "ریزش مو", "خارش"] },
|
||||||
|
"رفع جرم دندان": { category: "special-care", symptoms: ["جرم دندان", "بوی بد دهان", "التهاب لثه"] },
|
||||||
|
"شستشوی چشم": { category: "special-care", query: "چشم" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const SYMPTOMS = [
|
||||||
|
"لنگیدن", "سختی در بلند شدن", "ریزش مو", "جرم دندان", "بیاشتهایی", "اسهال مزمن", "بعد از مصرف آنتیبیوتیک"
|
||||||
|
];
|
||||||
|
|
||||||
|
const ArchiveProductCard: React.FC<{ product: Product, onClick: (p: Product) => void }> = ({ product, onClick }) => {
|
||||||
|
const { getActivePet } = usePetStore();
|
||||||
|
const { addItem } = useCartStore();
|
||||||
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
const activePet = getActivePet();
|
||||||
|
|
||||||
|
const handleAddToCart = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsAdding(true);
|
||||||
|
addItem(product, 1);
|
||||||
|
// @ts-ignore
|
||||||
|
import("sonner").then(({ toast }) => {
|
||||||
|
toast.success(`${product.name} به سبد خرید اضافه شد`);
|
||||||
|
});
|
||||||
|
setTimeout(() => setIsAdding(false), 800);
|
||||||
|
};
|
||||||
|
|
||||||
|
const compatibility = useMemo(() => {
|
||||||
|
if (!activePet) return null;
|
||||||
|
|
||||||
|
const sameSpecies = product.suitableFor === activePet.type || product.suitableFor === "هر دو";
|
||||||
|
const helpsSymptom = (activePet.medicalConditions || []).some(s => product.symptoms.includes(s));
|
||||||
|
|
||||||
|
if (!sameSpecies) return { type: 'alert', text: `مخصوص ${product.suitableFor}` };
|
||||||
|
if (helpsSymptom) return { type: 'success', text: `توصیه شده برای راهکار ${activePet.name}` };
|
||||||
|
if (sameSpecies) return { type: 'neutral', text: `مناسب برای ${activePet.name}` };
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [product, activePet]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
layout
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
onClick={() => onClick(product)}
|
||||||
|
className="group bg-white rounded-[2.5rem] border border-medical-gray-200 overflow-hidden hover:shadow-2xl transition-all cursor-pointer flex flex-col h-full relative"
|
||||||
|
>
|
||||||
|
{product.specialBadge && (
|
||||||
|
<div className="absolute top-4 right-4 z-10 bg-canina-blue text-white text-[9px] font-black uppercase tracking-widest px-3 py-1 rounded-full shadow-lg">
|
||||||
|
{product.specialBadge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{compatibility && (
|
||||||
|
<div className={`absolute top-14 right-4 z-10 text-[8px] font-black uppercase tracking-widest px-3 py-1 rounded-full shadow-md flex items-center gap-1 ${
|
||||||
|
compatibility.type === 'alert' ? 'bg-amber-100 text-amber-700' :
|
||||||
|
compatibility.type === 'success' ? 'bg-green-100 text-green-700' : 'bg-medical-gray-100 text-medical-gray-600'
|
||||||
|
}`}>
|
||||||
|
{compatibility.type === 'alert' ? <AlertCircle className="w-2.5 h-2.5" /> : compatibility.type === 'success' ? <Heart className="w-2.5 h-2.5" /> : <Sparkles className="w-2.5 h-2.5" />}
|
||||||
|
{compatibility.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="aspect-square bg-medical-gray-50 flex items-center justify-center p-8 overflow-hidden relative">
|
||||||
|
<SafeImage src={product.image} alt={product.name} className="w-full h-full object-contain group-hover:scale-110 transition-transform duration-700" />
|
||||||
|
<div className="absolute bottom-4 left-4 flex gap-2">
|
||||||
|
{(product.suitableFor === "سگ" || product.suitableFor === "هر دو") && (
|
||||||
|
<div className="w-7 h-7 bg-white rounded-lg flex items-center justify-center text-medical-gray-400 border border-medical-gray-100 shadow-sm">
|
||||||
|
<Dog className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(product.suitableFor === "گربه" || product.suitableFor === "هر دو") && (
|
||||||
|
<div className="w-7 h-7 bg-white rounded-lg flex items-center justify-center text-medical-gray-400 border border-medical-gray-100 shadow-sm">
|
||||||
|
<Cat className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 flex flex-col flex-1">
|
||||||
|
<div className="text-[10px] font-bold text-canina-blue uppercase tracking-widest mb-2">{product.category}</div>
|
||||||
|
<h3 className="text-lg font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors mb-3 leading-tight">{product.name}</h3>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-1 mb-6">
|
||||||
|
{product.symptoms.slice(0, 2).map((s, i) => (
|
||||||
|
<span key={i} className="text-[9px] font-bold bg-medical-gray-100 text-medical-gray-500 px-2 py-0.5 rounded-md">
|
||||||
|
{s}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-auto flex items-center justify-between pt-4 border-t border-medical-gray-50">
|
||||||
|
<span className="font-black text-medical-gray-900 font-vazir">{product.price}</span>
|
||||||
|
<button
|
||||||
|
onClick={handleAddToCart}
|
||||||
|
disabled={isAdding}
|
||||||
|
className="w-10 h-10 rounded-xl bg-medical-gray-50 text-canina-blue flex items-center justify-center group-hover:bg-canina-blue group-hover:text-white transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isAdding ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ArchivePage({
|
||||||
|
onProductClick,
|
||||||
|
onBack,
|
||||||
|
onShopNavigate,
|
||||||
|
initialCategory = "all",
|
||||||
|
initialSearch = ""
|
||||||
|
}: {
|
||||||
|
onProductClick: (p: Product) => void,
|
||||||
|
onBack?: () => void,
|
||||||
|
onShopNavigate?: (c?: string, s?: string) => void,
|
||||||
|
initialCategory?: string,
|
||||||
|
initialSearch?: string
|
||||||
|
}) {
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState(initialCategory);
|
||||||
|
const [selectedPet, setSelectedPet] = useState<PetType | "all">("all");
|
||||||
|
const [activeSymptoms, setActiveSymptoms] = useState<string[]>([]);
|
||||||
|
const [searchQuery, setSearchQuery] = useState(initialSearch);
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);
|
||||||
|
|
||||||
|
// Debug Log for Search Params
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("[ArchivePage] URL Params Sync:", { initialCategory, initialSearch });
|
||||||
|
}, [initialCategory, initialSearch]);
|
||||||
|
|
||||||
|
// Sync state with prop change (e.g. from Header menu)
|
||||||
|
useEffect(() => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
|
||||||
|
// Normalize Input (Trim and lowercase)
|
||||||
|
const normSearch = (initialSearch || "").trim().toLowerCase();
|
||||||
|
const normCat = initialCategory.trim().toLowerCase();
|
||||||
|
|
||||||
|
const mapKey = normSearch || normCat;
|
||||||
|
const mapping = CATEGORY_MAP[mapKey];
|
||||||
|
|
||||||
|
console.log("[ArchivePage] Processing Mapping for:", mapKey);
|
||||||
|
|
||||||
|
// Reset ALL other filters when a new category/solution is selected from menu
|
||||||
|
setSelectedPet("all");
|
||||||
|
setActiveSymptoms([]);
|
||||||
|
setSearchQuery("");
|
||||||
|
|
||||||
|
if (mapping) {
|
||||||
|
if (mapping.category) setSelectedCategory(mapping.category);
|
||||||
|
if (mapping.query) setSearchQuery(mapping.query);
|
||||||
|
if (mapping.symptoms) {
|
||||||
|
setActiveSymptoms(mapping.symptoms);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSelectedCategory(initialCategory);
|
||||||
|
setSearchQuery(initialSearch);
|
||||||
|
|
||||||
|
if (initialSearch && SYMPTOMS.includes(initialSearch)) {
|
||||||
|
setActiveSymptoms([initialSearch]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}, 400);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [initialCategory, initialSearch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchProducts = async () => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
try {
|
||||||
|
const data = await productService.getProducts({
|
||||||
|
category: selectedCategory,
|
||||||
|
petType: selectedPet,
|
||||||
|
query: searchQuery
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = data;
|
||||||
|
if (activeSymptoms.length > 0) {
|
||||||
|
result = result.filter(p => p.symptoms.some(s => activeSymptoms.includes(s)));
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredProducts(result);
|
||||||
|
} finally {
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchProducts();
|
||||||
|
}, [selectedCategory, selectedPet, searchQuery, activeSymptoms]);
|
||||||
|
|
||||||
|
const toggleSymptom = (s: string) => {
|
||||||
|
setActiveSymptoms(prev => prev.includes(s) ? prev.filter(item => item !== s) : [...prev, s]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-medical-gray-50 pb-20 px-4 pt-10 font-vazir" dir="rtl">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
|
||||||
|
{/* Mobile Navigation & Breadcrumbs */}
|
||||||
|
<div className="lg:hidden mb-10 space-y-4">
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="flex items-center gap-2 text-medical-gray-500 font-bold hover:text-canina-blue transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5 font-vazir" />
|
||||||
|
<span>بازگشت</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest overflow-x-auto whitespace-nowrap pb-2">
|
||||||
|
<span className="cursor-pointer hover:text-canina-blue" onClick={onBack}>کانینا</span>
|
||||||
|
<ChevronLeft className="w-2.5 h-2.5" />
|
||||||
|
<span className="text-canina-blue">فروشگاه تخصصی</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Breadcrumbs */}
|
||||||
|
<div className="hidden lg:flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-10">
|
||||||
|
<span className="cursor-pointer hover:text-canina-blue" onClick={onBack}>خانه</span>
|
||||||
|
<ChevronLeft className="w-2.5 h-2.5" />
|
||||||
|
<span className="text-canina-blue">کاتالوگ کامل محصولات</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row gap-10">
|
||||||
|
|
||||||
|
{/* Sidebar Filters */}
|
||||||
|
<aside className="lg:w-72 flex-shrink-0 space-y-8">
|
||||||
|
<div className="bg-white rounded-[2rem] p-6 border border-medical-gray-200 shadow-sm">
|
||||||
|
<div className="flex items-center gap-2 mb-6">
|
||||||
|
<Filter className="w-5 h-5 text-canina-blue" />
|
||||||
|
<h3 className="font-black text-medical-gray-900">فیلترهای تخصصی</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Pet Type */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block mb-3">نوع پت</span>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{[
|
||||||
|
{ id: "all", label: "هر دو", icon: <Activity className="w-3 h-3" /> },
|
||||||
|
{ id: "سگ", label: "سگ", icon: <Dog className="w-3 h-3" /> },
|
||||||
|
{ id: "گربه", label: "گربه", icon: <Cat className="w-3 h-3" /> },
|
||||||
|
].map(pet => (
|
||||||
|
<button
|
||||||
|
key={pet.id}
|
||||||
|
onClick={() => setSelectedPet(pet.id as any)}
|
||||||
|
className={`py-2 rounded-xl text-[10px] font-black border transition-all flex flex-col items-center gap-1 ${selectedPet === pet.id ? 'bg-canina-blue border-canina-blue text-white shadow-lg shadow-canina-blue/20' : 'bg-white border-medical-gray-200 text-medical-gray-400'}`}
|
||||||
|
>
|
||||||
|
{pet.icon}
|
||||||
|
{pet.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Solutions */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block mb-3">راهکار درمانی</span>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{CATEGORIES.map(cat => (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
onClick={() => setSelectedCategory(cat.id)}
|
||||||
|
className={`flex items-center gap-3 px-4 py-3 rounded-2xl text-xs font-bold transition-all border ${selectedCategory === cat.id ? 'bg-canina-blue border-canina-blue text-white' : 'bg-white border-medical-gray-100 text-medical-gray-600 hover:border-canina-blue/30'}`}
|
||||||
|
>
|
||||||
|
{cat.icon}
|
||||||
|
{cat.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Symptoms Checklist */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block mb-3">جستجو بر اساس علائم</span>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{SYMPTOMS.map(s => (
|
||||||
|
<button
|
||||||
|
key={s}
|
||||||
|
onClick={() => toggleSymptom(s)}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-[10px] font-bold transition-all border ${activeSymptoms.includes(s) ? 'bg-medical-gray-900 border-medical-gray-900 text-white' : 'bg-medical-gray-50 border-medical-gray-100 text-medical-gray-500 hover:border-medical-gray-300'}`}
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Promo Card */}
|
||||||
|
<div className="bg-medical-gray-900 rounded-[2rem] p-6 text-white relative overflow-hidden group">
|
||||||
|
<div className="absolute -top-10 -left-10 w-32 h-32 bg-canina-blue rounded-full blur-3xl opacity-20 group-hover:opacity-40 transition-opacity" />
|
||||||
|
<h4 className="text-lg font-black italic mb-4 relative z-10 underline decoration-canina-blue underline-offset-8">مشاوره رایگان</h4>
|
||||||
|
<p className="text-xs text-white/60 leading-relaxed mb-6 relative z-10">اگر نمیدانید کدام ترکیب برای پت شما مناسب است، همین حالا با دامپزشکان ما تماس بگیرید.</p>
|
||||||
|
<a href="tel:0218888" className="w-full bg-white text-medical-gray-900 py-3 rounded-xl text-xs font-black hover:scale-105 transition-transform relative z-10 flex items-center justify-center">شماره تماس: ۰۲۱-۸۸۸۸</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="flex-1 relative">
|
||||||
|
<div className="flex flex-col md:flex-row items-center justify-between mb-8 gap-4">
|
||||||
|
<h2 className="text-3xl font-black text-medical-gray-900">کاتولوگ دارویی <span className="text-canina-blue">Canina</span></h2>
|
||||||
|
<div className="relative w-full md:w-80">
|
||||||
|
<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-white 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>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{isUpdating ? (
|
||||||
|
<div key="skeleton" className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<ProductCardSkeleton key={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : filteredProducts.length > 0 ? (
|
||||||
|
<div key="grid" className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{filteredProducts.map((p: Product) => (
|
||||||
|
<ArchiveProductCard key={p.id} product={p} onClick={onProductClick} />
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div key="empty" className="bg-white rounded-[3rem] p-20 text-center border-2 border-dashed border-medical-gray-200 flex flex-col items-center">
|
||||||
|
<div className="w-20 h-20 bg-medical-gray-50 rounded-full flex items-center justify-center text-medical-gray-300 mb-6">
|
||||||
|
<Search className="w-10 h-10" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-black text-medical-gray-900 mb-2">محصولی یافت نشد!</h3>
|
||||||
|
<p className="text-medical-gray-400 max-w-sm mb-8 font-bold">با فیلترهای فعلی محصولی مطابق با نیاز شما پیدا نکردیم. لطفاً فیلترها را گستردهتر کنید یا دکمه ریست را بزنید.</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (onShopNavigate) {
|
||||||
|
onShopNavigate("all");
|
||||||
|
} else {
|
||||||
|
setSelectedCategory("all");
|
||||||
|
setSelectedPet("all");
|
||||||
|
setActiveSymptoms([]);
|
||||||
|
setSearchQuery("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
src/components/AuthModal.tsx
Normal file
97
src/components/AuthModal.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { X, User, Building2, Heart, ShieldCheck } from "lucide-react";
|
||||||
|
import { useUserStore, UserRole } from "../store/userStore";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface AuthModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
||||||
|
const { setRole, setLoggedIn } = useUserStore();
|
||||||
|
|
||||||
|
const handleQuickLogin = (role: UserRole) => {
|
||||||
|
// TODO: [BACKEND_API] POST /api/auth/login | Payload: { role: UserRole, mobile?: string } | Expected: { token: string, user: UserProfile } | Errors: [401]
|
||||||
|
setRole(role);
|
||||||
|
setLoggedIn(role !== "User_Guest");
|
||||||
|
toast.success(`خوش آمدید! در نقش ${role === "User_Partner" ? 'همکار' : role === 'User_PetOwner' ? 'صاحب پت' : 'مهمان'} وارد شدید.`);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[60]"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white rounded-[2.5rem] shadow-2xl z-[70] overflow-hidden border border-medical-gray-100"
|
||||||
|
>
|
||||||
|
<div className="p-8 relative">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-6 left-6 p-2 rounded-full hover:bg-medical-gray-50 text-medical-gray-400 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center text-center mb-10 pt-4">
|
||||||
|
<div className="w-20 h-20 bg-canina-blue/10 rounded-3xl flex items-center justify-center mb-6">
|
||||||
|
<ShieldCheck className="w-10 h-10 text-canina-blue" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl font-black text-medical-gray-900 font-vazir mb-3">ورود به دنیای کانینا</h2>
|
||||||
|
<p className="text-medical-gray-500 font-medium font-vazir max-w-[280px]">
|
||||||
|
برای دسترسی به پرونده سلامت و تخفیفات اختصاصی وارد شوید
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<button className="w-full bg-medical-gray-50 border border-medical-gray-100 rounded-2xl p-4 flex items-center justify-between group hover:border-canina-blue transition-all">
|
||||||
|
<span className="font-bold text-sm text-medical-gray-600 font-vazir">ورود با شماره موبایل</span>
|
||||||
|
<div className="w-8 h-8 rounded-full bg-white flex items-center justify-center shadow-sm text-canina-blue">→</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-12 pt-8 border-t border-medical-gray-50">
|
||||||
|
<p className="text-[10px] font-black text-medical-gray-300 uppercase tracking-widest text-center mb-6 font-vazir">ورود سریع (جهت تست)</p>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => handleQuickLogin("User_Guest")}
|
||||||
|
className="flex flex-col items-center gap-2 p-3 rounded-2xl border border-medical-gray-100 hover:bg-medical-gray-50 transition-all group"
|
||||||
|
>
|
||||||
|
<User className="w-6 h-6 text-medical-gray-400 group-hover:text-canina-blue" />
|
||||||
|
<span className="text-[10px] font-bold font-vazir">مهمان</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleQuickLogin("User_PetOwner")}
|
||||||
|
className="flex flex-col items-center gap-2 p-3 rounded-2xl border border-medical-gray-100 hover:bg-medical-gray-50 transition-all group"
|
||||||
|
>
|
||||||
|
<Heart className="w-6 h-6 text-medical-gray-400 group-hover:text-canina-blue" />
|
||||||
|
<span className="text-[10px] font-bold font-vazir">صاحب پت</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleQuickLogin("User_Partner")}
|
||||||
|
className="flex flex-col items-center gap-2 p-3 rounded-2xl border border-medical-gray-100 hover:bg-medical-gray-50 transition-all group"
|
||||||
|
>
|
||||||
|
<Building2 className="w-6 h-6 text-medical-gray-400 group-hover:text-canina-blue" />
|
||||||
|
<span className="text-[10px] font-bold font-vazir">همکار</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
185
src/components/B2BPortal.tsx
Normal file
185
src/components/B2BPortal.tsx
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { PRODUCTS, Product } from "../data/products";
|
||||||
|
import { useCartStore } from "../store/cartStore";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import {
|
||||||
|
Building2,
|
||||||
|
ShoppingCart,
|
||||||
|
Search,
|
||||||
|
ChevronRight,
|
||||||
|
FileText,
|
||||||
|
Package,
|
||||||
|
CheckCircle2,
|
||||||
|
X
|
||||||
|
} from "lucide-react";
|
||||||
|
import SafeImage from "./SafeImage";
|
||||||
|
|
||||||
|
interface QuickOrderItem {
|
||||||
|
product: Product;
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function B2BPortal({ onClose }: { onClose: () => void }) {
|
||||||
|
const [quantities, setQuantities] = useState<Record<string, number>>({});
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const { addItem } = useCartStore();
|
||||||
|
const [showSuccess, setShowSuccess] = useState(false);
|
||||||
|
|
||||||
|
const filteredProducts = PRODUCTS.filter(p =>
|
||||||
|
p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
p.artNo.includes(searchQuery)
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleQuantityChange = (id: string, val: string) => {
|
||||||
|
const num = parseInt(val) || 0;
|
||||||
|
setQuantities(prev => ({ ...prev, [id]: Math.max(0, num) }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddAll = () => {
|
||||||
|
Object.entries(quantities).forEach(([id, qty]) => {
|
||||||
|
const numQty = qty as number;
|
||||||
|
if (numQty > 0) {
|
||||||
|
const product = PRODUCTS.find(p => p.id === id);
|
||||||
|
if (product) {
|
||||||
|
addItem(product, numQty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setShowSuccess(true);
|
||||||
|
setTimeout(() => setShowSuccess(false), 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalItems = Object.values(quantities).reduce((acc: number, val) => acc + (val as any), 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 z-[100] bg-medical-gray-900/60 backdrop-blur-md flex items-center justify-center p-4 md:p-10 font-vazir"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||||
|
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||||
|
className="bg-white w-full max-w-6xl h-[90vh] rounded-[3rem] shadow-2xl overflow-hidden flex flex-col md:flex-row relative"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-6 left-6 z-20 w-10 h-10 bg-medical-gray-100 rounded-full flex items-center justify-center text-medical-gray-500 hover:bg-red-50 hover:text-red-500 transition-all"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Sidebar Info */}
|
||||||
|
<div className="w-full md:w-80 bg-canina-blue p-10 text-white flex flex-col">
|
||||||
|
<div className="mb-10">
|
||||||
|
<div className="w-16 h-16 bg-white/20 rounded-2xl flex items-center justify-center mb-6">
|
||||||
|
<Building2 className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-black mb-4 italic">پنل کلینیکها <br/> و داروخانهها</h2>
|
||||||
|
<p className="text-white/60 text-sm leading-relaxed">
|
||||||
|
این بخش اختصاصی برای ثبت سفارشات حجیم و استعلام موجودی آنی طراحی شده است. قیمتهای نمایش داده شده، فی واحد کاتالوگ هستند.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-auto space-y-4">
|
||||||
|
<div className="p-4 bg-white/10 rounded-2xl flex items-center gap-3">
|
||||||
|
<FileText className="w-5 h-5 text-blue-300" />
|
||||||
|
<span className="text-xs font-bold">دریافت لیست قیمت PDF</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-white/10 rounded-2xl flex items-center gap-3">
|
||||||
|
<Package className="w-5 h-5 text-blue-300" />
|
||||||
|
<span className="text-xs font-bold">پیگیری محمولات قبلی</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Grid Area */}
|
||||||
|
<div className="flex-1 flex flex-col bg-medical-gray-50 overflow-hidden">
|
||||||
|
{/* Header & Search */}
|
||||||
|
<div className="p-8 border-b border-medical-gray-200 flex flex-col md:flex-row md:items-center justify-between gap-6 bg-white">
|
||||||
|
<div className="relative flex-1 max-w-md">
|
||||||
|
<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-2xl py-3 pr-11 pl-4 text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleAddAll}
|
||||||
|
disabled={totalItems === 0}
|
||||||
|
className="bg-medical-gray-900 text-white px-8 py-3 rounded-2xl font-black text-sm flex items-center gap-2 hover:bg-canina-blue transition-colors disabled:opacity-30"
|
||||||
|
>
|
||||||
|
<ShoppingCart className="w-4 h-4" />
|
||||||
|
افزودن کلی ({totalItems} عدد)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-8">
|
||||||
|
<div className="bg-white border border-medical-gray-200 rounded-3xl overflow-hidden shadow-sm">
|
||||||
|
<table className="w-full text-right">
|
||||||
|
<thead className="bg-medical-gray-50 border-b border-medical-gray-200">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">تصویر</th>
|
||||||
|
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">نام و کد کالا</th>
|
||||||
|
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">وضعیت موجودی</th>
|
||||||
|
<th className="px-6 py-4 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">تعداد سفارش</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-medical-gray-100">
|
||||||
|
{filteredProducts.map(product => (
|
||||||
|
<tr key={product.id} className="hover:bg-medical-gray-50/50 transition-colors">
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<SafeImage src={product.image} className="w-12 h-12 object-contain" alt={product.name} />
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<div className="font-black text-medical-gray-900">{product.name}</div>
|
||||||
|
<div className="text-[10px] font-vazir text-medical-gray-400 mt-1">شناسه کالا: {product.artNo}</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-green-50 text-green-600 rounded-full text-[10px] font-bold">
|
||||||
|
<CheckCircle2 className="w-3 h-3" />
|
||||||
|
آماده ارسال
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={quantities[product.id] || ""}
|
||||||
|
onChange={(e) => handleQuantityChange(product.id, e.target.value)}
|
||||||
|
placeholder="0"
|
||||||
|
className="w-20 bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2 px-3 text-center font-mono font-bold focus:ring-2 focus:ring-canina-blue/20 outline-none"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{showSuccess && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 50 }}
|
||||||
|
className="absolute bottom-10 right-10 bg-green-600 text-white px-6 py-4 rounded-2xl shadow-xl flex items-center gap-3 z-50"
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-5 h-5" />
|
||||||
|
<span className="font-bold text-sm">سفارشات با موفقیت به سبد خرید اضافه شدند.</span>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
335
src/components/BlogPage.tsx
Normal file
335
src/components/BlogPage.tsx
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { ChevronRight, Calendar, User, ArrowLeft, Sparkles, BookOpen } from "lucide-react";
|
||||||
|
import { Product, PRODUCTS } from "../data/products";
|
||||||
|
|
||||||
|
const BLOG_POSTS = [
|
||||||
|
{
|
||||||
|
id: "p1",
|
||||||
|
title: "مزایای شگفتانگیز صدف لبسبز نیوزیلند برای مفاصل پت",
|
||||||
|
excerpt: "صدف لبسبز (Perna Canaliculus) تنها منبع طبیعی حاوی گلیکوزآمینگلیکانها (GAG) در غلظت بسیار بالا محسوب میشود.",
|
||||||
|
image: "https://images.unsplash.com/photo-1599443015574-be5fe8a05783?auto=format&fit=crop&q=80&w=800",
|
||||||
|
date: "۲۵ اردیبهشت ۱۴۰۳",
|
||||||
|
author: "دکتر کلاوس هنینگ",
|
||||||
|
content: `
|
||||||
|
صدف لبسبز نیوزیلندی قلب تپنده محصولات ارتوپدی کانینا است. این صدف که منحصراً در آبهای فوقالعاده تمیز نیوزیلند تکثیر میشود، حاوی ترکیبات ضدالتهابی طبیعی است که در هیچ منبع دیگری به این غلظت یافت نمیشود.
|
||||||
|
|
||||||
|
چرا برای سگهای نژاد بزرگ حیاتی است؟
|
||||||
|
در سگهای نژاد بزرگ، سرعت رشد اسکلتی بسیار بالاست. اگر مفاصل و تاندونها با این سرعت هماهنگ نشوند، بدشکلیهای ماندگار ایجاد میشود. GAG موجود در صدف، مانند چسب بیولوژیک عمل کرده و بافت غضروفی را ترمیم میکند.
|
||||||
|
|
||||||
|
مکمل Canhydrox GAG با دارا بودن ۱۵٪ پودر صدف لبسبز خالص، استاندارد طلایی در دامپزشکی آلمان است.
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "p2",
|
||||||
|
title: "چگونه سیستم ایمنی پت خود را در برابر بیماریهای فصلی تقویت کنیم؟",
|
||||||
|
excerpt: "آغوز (Colostrum) اولین سد دفاعی طبیعت است. با ایمیونبوستر کانینا، پت شما در برابر ویروسها بیمه میشود.",
|
||||||
|
image: "https://images.unsplash.com/photo-1576091160550-217359f42f8c?auto=format&fit=crop&q=80&w=800",
|
||||||
|
date: "۲۰ اردیبهشت ۱۴۰۳",
|
||||||
|
author: "دکتر النا اشمیت",
|
||||||
|
content: "ایمنی بدن یک مکانیزم پیچیده است. استفاده از Immun-Booster به ویژه در دوران واکسیناسیون یا جابجایی، به بدن پت کمک میکند تا آنتیبادیهای لازم را با سرعت بیشتری تولید کند."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "p3",
|
||||||
|
title: "اهمیت اسیدهای چرب امگا ۳ در درخشش پوشش مویی",
|
||||||
|
excerpt: "ریزش موی شدید در سگ و گربه معمولاً نشانه کمبود اسیدهای چرب ضروری EPA و DHA است. روغن سالمون پرس سرد راه حل نهایی است.",
|
||||||
|
image: "https://images.unsplash.com/photo-1516734212186-a967f81ad0d7?auto=format&fit=crop&q=80&w=800",
|
||||||
|
date: "۱۵ اردیبهشت ۱۴۰۳",
|
||||||
|
author: "دکتر ماری کونیگ",
|
||||||
|
content: "روغن سالمون کانینا به روش پرس سرد تهیه شده تا تمامی ویتامینهای حساس به حرارت حفظ شوند. این روغن نه تنها پوست را مرطوب نگه میدارد، بلکه التهابات مفصلی را نیز کاهش میدهد."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function BlogPage({ onBack, onProductClick }: { onBack: () => void, onProductClick: (p: Product) => void }) {
|
||||||
|
const [selectedPost, setSelectedPost] = React.useState<typeof BLOG_POSTS[0] | null>(null);
|
||||||
|
|
||||||
|
if (selectedPost) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-medical-gray-50 pt-8 pb-20 px-4 font-vazir" dir="rtl">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Breadcrumbs */}
|
||||||
|
<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={onBack}>خانه</span>
|
||||||
|
<ChevronRight className="w-2.5 h-2.5" />
|
||||||
|
<span className="cursor-pointer hover:text-canina-blue" onClick={() => setSelectedPost(null)}>مجله سلامت</span>
|
||||||
|
<ChevronRight className="w-2.5 h-2.5" />
|
||||||
|
<span className="text-canina-blue">{selectedPost.title}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid lg:grid-cols-12 gap-12">
|
||||||
|
<div className="lg:col-span-8">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-[3.5rem] border border-medical-gray-200 shadow-2xl overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="aspect-video w-full overflow-hidden">
|
||||||
|
<img src={selectedPost.image} alt={selectedPost.title} className="w-full h-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<div className="p-12">
|
||||||
|
<div className="flex items-center gap-4 text-xs font-bold text-canina-blue mb-8">
|
||||||
|
<div className="bg-canina-blue/10 px-4 py-1.5 rounded-full uppercase tracking-widest">تحقیق بالینی</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
{selectedPost.date}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl lg:text-5xl font-black text-medical-gray-900 mb-10 leading-tight">
|
||||||
|
{selectedPost.title}
|
||||||
|
</h1>
|
||||||
|
<div className="prose prose-lg max-w-none text-medical-gray-600 leading-[2] text-justify space-y-6">
|
||||||
|
<p className="font-bold text-medical-gray-900 italic text-xl border-r-4 border-canina-blue pr-6 py-2">
|
||||||
|
{selectedPost.excerpt}
|
||||||
|
</p>
|
||||||
|
<div className="whitespace-pre-line">
|
||||||
|
{selectedPost.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-16 pt-10 border-t border-medical-gray-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-14 h-14 bg-medical-gray-100 rounded-full flex items-center justify-center text-medical-gray-400 border-2 border-white shadow-lg">
|
||||||
|
<User className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-black text-medical-gray-900 text-lg">{selectedPost.author}</div>
|
||||||
|
<div className="font-bold text-medical-gray-400">تیم تحقیق و توسعه کانینا آلمان</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button className="p-4 bg-medical-gray-100 rounded-2xl hover:bg-canina-blue hover:text-white transition-all text-medical-gray-400">
|
||||||
|
<Sparkles className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:col-span-4 space-y-10">
|
||||||
|
<div className="bg-canina-blue rounded-[3rem] p-10 text-white shadow-2xl relative overflow-hidden">
|
||||||
|
<div className="absolute top-0 left-0 p-8 opacity-10">
|
||||||
|
<Sparkles className="w-20 h-20" />
|
||||||
|
</div>
|
||||||
|
<h4 className="text-2xl font-black mb-6 italic relative z-10">محصولات پیشنهادی</h4>
|
||||||
|
<div className="space-y-6 relative z-10">
|
||||||
|
{PRODUCTS.slice(4, 7).map(p => (
|
||||||
|
<div
|
||||||
|
key={p.id}
|
||||||
|
onClick={() => onProductClick(p)}
|
||||||
|
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">
|
||||||
|
<img src={p.image} alt={p.name} className="w-full h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] font-black opacity-50 uppercase tracking-widest">{p.category}</div>
|
||||||
|
<div className="text-sm font-black font-vazir text-white">{p.name}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-[3rem] border border-medical-gray-200 p-10 shadow-xl">
|
||||||
|
<h4 className="text-xl font-black italic mb-6">محبوبترین مقالات</h4>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{BLOG_POSTS.map(p => (
|
||||||
|
<div
|
||||||
|
key={p.id}
|
||||||
|
onClick={() => setSelectedPost(p)}
|
||||||
|
className="flex items-center gap-4 cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div className="w-16 h-16 rounded-2xl overflow-hidden shrink-0">
|
||||||
|
<img src={p.image} className="w-full h-full object-cover group-hover:scale-110 transition-transform" />
|
||||||
|
</div>
|
||||||
|
<div className="text-sm font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors line-clamp-2">
|
||||||
|
{p.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-medical-gray-50 pt-8 pb-20 px-4 font-vazir" dir="rtl">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col md:flex-row items-center justify-between mb-16 gap-6">
|
||||||
|
<div className="text-right">
|
||||||
|
<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">
|
||||||
|
<BookOpen className="w-3 h-3" />
|
||||||
|
Canina Science Blog
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl lg:text-6xl font-black text-medical-gray-900 italic leading-tight">
|
||||||
|
مجله تخصصی <span className="text-canina-blue">سلامت پتها</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-medical-gray-500 font-medium mt-4 max-w-xl">
|
||||||
|
آخرین یافتههای علمی محققین کانینا آلمان در مورد تغذیه و سلامت پتها.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="px-8 py-3 bg-white border border-medical-gray-200 rounded-2xl text-medical-gray-700 shadow-sm flex items-center gap-2 font-black hover:bg-medical-gray-50 transition-all"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
بازگشت به خانه
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Featured Post */}
|
||||||
|
<div className="mb-20">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-[3.5rem] overflow-hidden border border-medical-gray-200 shadow-2xl flex flex-col lg:flex-row cursor-pointer group"
|
||||||
|
onClick={() => setSelectedPost(BLOG_POSTS[0])}
|
||||||
|
>
|
||||||
|
<div className="lg:w-1/2 aspect-video lg:aspect-auto">
|
||||||
|
<img
|
||||||
|
src={BLOG_POSTS[0].image}
|
||||||
|
alt={BLOG_POSTS[0].title}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="lg:w-1/2 p-12 flex flex-col justify-center">
|
||||||
|
<div className="flex items-center gap-4 text-xs font-bold text-canina-blue mb-6">
|
||||||
|
<div className="bg-canina-blue/10 px-3 py-1 rounded-full uppercase tracking-widest">تخصصی</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Calendar className="w-3 h-3" />
|
||||||
|
{BLOG_POSTS[0].date}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl lg:text-4xl font-black text-medical-gray-900 mb-6 leading-tight group-hover:text-canina-blue transition-colors">
|
||||||
|
{BLOG_POSTS[0].title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-medical-gray-500 mb-8 leading-relaxed italic">
|
||||||
|
{BLOG_POSTS[0].excerpt}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between pt-8 border-t border-medical-gray-100">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-medical-gray-100 rounded-full flex items-center justify-center text-medical-gray-400">
|
||||||
|
<User className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="font-black text-medical-gray-900">{BLOG_POSTS[0].author}</div>
|
||||||
|
<div className="font-bold text-medical-gray-400">محقق ارشد کانینا</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-canina-blue font-black">
|
||||||
|
ادامه مطلب
|
||||||
|
<ArrowLeft className="w-5 h-5 group-hover:-translate-x-2 transition-transform" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Post Grid */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
{BLOG_POSTS.slice(1).map((post, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={post.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className="bg-white rounded-[3rem] overflow-hidden border border-medical-gray-200 shadow-xl group hover:shadow-2xl transition-all cursor-pointer"
|
||||||
|
onClick={() => setSelectedPost(post)}
|
||||||
|
>
|
||||||
|
<div className="aspect-[16/9] overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={post.image}
|
||||||
|
alt={post.title}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-8">
|
||||||
|
<div className="flex items-center gap-4 text-[10px] font-bold text-canina-blue mb-4">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Calendar className="w-3 h-3" />
|
||||||
|
{post.date}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-black text-medical-gray-900 mb-4 group-hover:text-canina-blue transition-colors">
|
||||||
|
{post.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-medical-gray-500 mb-8 leading-relaxed line-clamp-2">
|
||||||
|
{post.excerpt}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 text-medical-gray-900 font-bold group">
|
||||||
|
مطالعه مقاله
|
||||||
|
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Newsletter / CTA Box */}
|
||||||
|
<div className="bg-canina-blue rounded-[3rem] p-10 text-white relative overflow-hidden flex flex-col justify-center">
|
||||||
|
<div className="absolute top-0 right-0 p-8 opacity-20">
|
||||||
|
<Sparkles className="w-24 h-24" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl font-black mb-4 italic">مشترک خبرنامه علمی شوید</h3>
|
||||||
|
<p className="text-white/80 mb-8 font-medium leading-relaxed">
|
||||||
|
ماهانه یک ایمیل حاوی مهمترین نکات سلامت پتها و کدهای تخفیف اختصاصی دریافت کنید.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="آدرس ایمیل شما"
|
||||||
|
className="flex-1 bg-white/10 border border-white/20 rounded-xl px-4 py-3 outline-none focus:bg-white/20 placeholder:text-white/40"
|
||||||
|
/>
|
||||||
|
<button className="bg-white text-canina-blue px-6 py-3 rounded-xl font-black">
|
||||||
|
عضویت
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Synergy Products Cross-sell */}
|
||||||
|
<section className="mt-32 p-12 bg-canina-blue rounded-[4rem] text-white overflow-hidden relative shadow-2xl">
|
||||||
|
<div className="absolute inset-0 opacity-20 pointer-events-none">
|
||||||
|
<div className="grid grid-cols-6 h-full w-full">
|
||||||
|
{Array.from({length: 36}).map((_, i) => (
|
||||||
|
<div key={i} className="border border-white/20" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative z-10 flex flex-col lg:flex-row items-center gap-12">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-3xl lg:text-4xl font-black italic mb-6">علم آلمانی در خدمت سلامت پت شما</h3>
|
||||||
|
<p className="text-white/80 mb-8 leading-relaxed max-w-xl">
|
||||||
|
تمامی مقالات این وبلاگ بر اساس کاتالوگهای رسمی و نتایج آزمایشگاهی کمپانی کانینا آلمان تدوین شده است.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => onBack()}
|
||||||
|
className="bg-white text-canina-blue px-8 py-4 rounded-2xl font-black hover:scale-105 transition-transform"
|
||||||
|
>
|
||||||
|
مشاهده محصولات تخصصی
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 flex-shrink-0">
|
||||||
|
{PRODUCTS.slice(0, 2).map((p) => (
|
||||||
|
<div
|
||||||
|
key={p.id}
|
||||||
|
onClick={() => onProductClick(p)}
|
||||||
|
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" />
|
||||||
|
<h5 className="text-[10px] font-black">{p.name}</h5>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
298
src/components/CartDrawer.tsx
Normal file
298
src/components/CartDrawer.tsx
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { X, Trash2, Plus, Minus, ShoppingBag, ShieldCheck, ArrowLeft, Ticket, Sparkles, CheckCircle2 } from "lucide-react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { toPersian, cn } from "../lib/utils";
|
||||||
|
import { useCartStore } from "../store/cartStore";
|
||||||
|
import { PRODUCTS } from "../data/products";
|
||||||
|
import SafeImage from "./SafeImage";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export default function CartDrawer({ isOpen, onClose, onCheckout, onShopNavigate }: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onCheckout: () => void;
|
||||||
|
onShopNavigate?: () => void;
|
||||||
|
}) {
|
||||||
|
const { items, updateQuantity, removeItem, isSubscribed, toggleSubscription, getTotal, getSubtotal, getDiscount, coupon, applyCoupon, addItem } = useCartStore();
|
||||||
|
const [couponCode, setCouponCode] = useState("");
|
||||||
|
const [couponStatus, setCouponStatus] = useState<"idle" | "success" | "error">("idle");
|
||||||
|
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleApplyCoupon = () => {
|
||||||
|
const success = applyCoupon(couponCode);
|
||||||
|
if (success) {
|
||||||
|
setCouponStatus("success");
|
||||||
|
setTimeout(() => setCouponStatus("idle"), 3000);
|
||||||
|
} else {
|
||||||
|
setCouponStatus("error");
|
||||||
|
setTimeout(() => setCouponStatus("idle"), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Smart Cross-sell Logic
|
||||||
|
const hasCanhydrox = items.some(i => i.product.id === 'canhydrox-gag');
|
||||||
|
const hasLachsOl = items.some(i => i.product.id === 'lachs-ol');
|
||||||
|
|
||||||
|
const suggestedProduct = (!hasLachsOl && hasCanhydrox)
|
||||||
|
? PRODUCTS.find(p => p.id === 'lachs-ol')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={onClose}
|
||||||
|
className="fixed inset-0 bg-medical-gray-900/40 backdrop-blur-sm z-[100]"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ x: "100%" }}
|
||||||
|
animate={{ x: 0 }}
|
||||||
|
exit={{ x: "100%" }}
|
||||||
|
transition={{ type: "spring", damping: 25, stiffness: 200 }}
|
||||||
|
className="fixed inset-y-0 right-0 w-full max-w-md bg-white shadow-2xl z-[101] flex flex-col"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-6 border-b border-medical-gray-100 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-medical-gray-50 rounded-xl flex items-center justify-center text-canina-blue">
|
||||||
|
<ShoppingBag className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-black text-medical-gray-900 italic">سبد خرید</h3>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClose} className="p-2 hover:bg-medical-gray-50 rounded-full transition-colors">
|
||||||
|
<X className="w-6 h-6 text-medical-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<div className="h-full flex flex-col items-center justify-center text-center space-y-4">
|
||||||
|
<div className="w-20 h-20 bg-medical-gray-50 rounded-full flex items-center justify-center text-medical-gray-300">
|
||||||
|
<ShoppingBag className="w-10 h-10" />
|
||||||
|
</div>
|
||||||
|
<p className="text-medical-gray-500 font-bold">سبد خرید شما خالی است</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
onShopNavigate?.();
|
||||||
|
}}
|
||||||
|
className="text-canina-blue font-black text-sm border-b-2 border-canina-blue pb-1 font-vazir"
|
||||||
|
>
|
||||||
|
مشاهده محصولات تخصصی
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.product.id} className="flex gap-4 p-4 bg-medical-gray-50 rounded-3xl border border-medical-gray-100 group">
|
||||||
|
<div className="w-20 h-20 bg-white rounded-2xl p-2 flex-shrink-0 flex items-center justify-center shadow-sm">
|
||||||
|
<SafeImage src={item.product.image} alt={item.product.name} className="w-full h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h4 className="text-sm font-black text-medical-gray-900 truncate mb-1">{item.product.name}</h4>
|
||||||
|
<p className="text-[10px] text-canina-blue font-bold mb-3">{item.product.category}</p>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2 bg-white rounded-lg border border-medical-gray-200 p-1">
|
||||||
|
<button
|
||||||
|
onClick={() => updateQuantity(item.product.id, item.quantity - 1)}
|
||||||
|
className="p-1 hover:text-canina-blue transition-colors"
|
||||||
|
>
|
||||||
|
<Minus className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
<span className="text-xs font-black min-w-[20px] text-center font-vazir">{toPersian(item.quantity)}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => updateQuantity(item.product.id, item.quantity + 1)}
|
||||||
|
className="p-1 hover:text-canina-blue transition-colors"
|
||||||
|
>
|
||||||
|
<Plus className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteConfirmId(item.product.id)}
|
||||||
|
className="text-medical-gray-300 hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-left flex flex-col justify-between items-end">
|
||||||
|
<span className="text-xs font-black text-medical-gray-900 font-vazir">{toPersian((item.product.priceValue * item.quantity).toLocaleString())}</span>
|
||||||
|
<span className="text-[10px] text-medical-gray-400 font-bold">تومان</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Delete Confirmation Overlay */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{deleteConfirmId === item.product.id && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="absolute inset-0 bg-white/95 z-40 flex flex-col items-center justify-center p-4 text-center rounded-3xl"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 bg-red-50 rounded-full flex items-center justify-center text-red-500 mb-2">
|
||||||
|
<Trash2 className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] font-black text-medical-gray-900 mb-3 font-vazir">
|
||||||
|
مطمئنی میخوای این محصول رو حذف کنی؟
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2 w-full px-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
removeItem(item.product.id);
|
||||||
|
setDeleteConfirmId(null);
|
||||||
|
toast.error(`${item.product.name} حذف شد`);
|
||||||
|
}}
|
||||||
|
className="flex-1 bg-red-500 text-white py-2 rounded-xl text-[10px] font-black shadow-lg shadow-red-500/20"
|
||||||
|
>
|
||||||
|
حذف
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteConfirmId(null)}
|
||||||
|
className="flex-1 bg-medical-gray-100 text-medical-gray-600 py-2 rounded-xl text-[10px] font-black"
|
||||||
|
>
|
||||||
|
لغو
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Smart Cross-sell Recommendation */}
|
||||||
|
{suggestedProduct && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="p-5 bg-gradient-to-br from-canina-blue/5 to-transparent border border-canina-blue/10 rounded-3xl"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Sparkles className="w-4 h-4 text-canina-blue animate-pulse" />
|
||||||
|
<span className="text-xs font-black text-medical-gray-900">پیشنهاد هوشمند برای جذب بهتر</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-16 h-16 bg-white rounded-2xl p-2 shrink-0 shadow-sm border border-canina-blue/5">
|
||||||
|
<SafeImage src={suggestedProduct.image} alt="" className="w-full h-full object-contain" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h5 className="text-[10px] font-black text-medical-gray-900 mb-1">{suggestedProduct.name}</h5>
|
||||||
|
<p className="text-[9px] text-medical-gray-500 font-medium leading-tight">برای جذب ویتامینهای Canhydrox، اسیدهای چرب سالمون ضروری هستند.</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => addItem(suggestedProduct, 1)}
|
||||||
|
className="bg-canina-blue text-white w-8 h-8 rounded-full flex items-center justify-center hover:scale-110 transition-transform shadow-lg shadow-canina-blue/20"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
{items.length > 0 && (
|
||||||
|
<div className="p-6 bg-medical-gray-50 border-t border-medical-gray-200 space-y-6">
|
||||||
|
{/* Discount Code */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest px-2">
|
||||||
|
<Ticket className="w-3 h-3" />
|
||||||
|
کد تخفیف اختصاصی
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="CANINA2024"
|
||||||
|
value={couponCode}
|
||||||
|
onChange={(e) => setCouponCode(e.target.value)}
|
||||||
|
className={`w-full bg-white border ${couponStatus === 'error' ? 'border-red-300' : 'border-medical-gray-200'} rounded-2xl py-3 px-4 pr-10 text-sm font-bold focus:outline-none focus:ring-2 focus:ring-canina-blue/20 transition-all`}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleApplyCoupon}
|
||||||
|
className={`absolute left-2 top-1/2 -translate-y-1/2 px-4 py-1.5 rounded-xl text-[10px] font-black transition-all ${couponStatus === 'success' ? 'bg-green-600 text-white' : 'bg-medical-gray-900 text-white hover:bg-canina-blue'}`}
|
||||||
|
>
|
||||||
|
{couponStatus === 'success' ? <CheckCircle2 className="w-3 h-3" /> : 'اعمال'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{coupon && (
|
||||||
|
<div className="flex items-center justify-between p-3 bg-green-50 border border-green-100 rounded-2xl">
|
||||||
|
<div className="flex items-center gap-2 text-[10px] font-bold text-green-600">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-green-600 animate-pulse" />
|
||||||
|
کد {coupon.code} ({(coupon.discount < 1 ? coupon.discount * 100 : coupon.discount)} {coupon.discount < 1 ? '٪' : 'تومان'}) اعمال شد
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
useCartStore.getState().removeCoupon();
|
||||||
|
setCouponCode("");
|
||||||
|
}}
|
||||||
|
className="p-1 hover:bg-red-50 hover:text-red-500 text-medical-gray-400 rounded-lg transition-all"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subscription Toggle */}
|
||||||
|
<div
|
||||||
|
onClick={toggleSubscription}
|
||||||
|
className={`p-4 rounded-2xl border-2 transition-all cursor-pointer ${isSubscribed ? 'bg-canina-blue/5 border-canina-blue' : 'bg-white border-medical-gray-100 hover:border-canina-blue/30'}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`w-10 h-10 rounded-xl flex items-center justify-center transition-colors ${isSubscribed ? 'bg-canina-blue text-white' : 'bg-medical-gray-50 text-medical-gray-300'}`}>
|
||||||
|
<ShieldCheck className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h5 className="text-xs font-black text-medical-gray-900">فعالسازی سرویس تمدید خودکار (Refill)</h5>
|
||||||
|
<p className="text-[10px] text-medical-gray-500 font-medium">۵٪ تخفیف روی تمام فاکتور + ارسال اولویتدار</p>
|
||||||
|
</div>
|
||||||
|
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${isSubscribed ? 'bg-canina-blue border-canina-blue' : 'border-medical-gray-200'}`}>
|
||||||
|
{isSubscribed && <X className="w-3 h-3 text-white" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-sm px-2">
|
||||||
|
<span className="text-medical-gray-500 font-bold">مجموع اقلام</span>
|
||||||
|
<span className="text-medical-gray-900 font-black font-vazir">{toPersian(getSubtotal().toLocaleString())} تومان</span>
|
||||||
|
</div>
|
||||||
|
{getDiscount() > 0 && (
|
||||||
|
<div className="flex items-center justify-between text-sm text-green-600 px-2 font-black">
|
||||||
|
<span className="font-bold">مجموع تخفیفها</span>
|
||||||
|
<span className="font-black font-vazir">-{toPersian(getDiscount().toLocaleString())} تومان</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center justify-between border-t border-medical-gray-200 pt-4 px-2">
|
||||||
|
<span className="text-lg font-black text-medical-gray-900 italic">مجموع قابل پرداخت</span>
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-2xl font-black text-canina-blue tracking-tighter leading-none font-vazir">{toPersian(getTotal().toLocaleString())}</p>
|
||||||
|
<p className="text-[10px] text-medical-gray-400 font-bold">تومان</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onCheckout}
|
||||||
|
className="w-full bg-canina-blue text-white py-5 rounded-[2.5rem] font-black text-lg hover:shadow-2xl hover:shadow-canina-blue/20 transition-all flex items-center justify-center gap-3 group"
|
||||||
|
>
|
||||||
|
ثبت نهایی و تسویه حساب
|
||||||
|
<ArrowLeft className="w-5 h-5 group-hover:translate-x-[-4px] transition-transform" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
371
src/components/CheckoutPage.tsx
Normal file
371
src/components/CheckoutPage.tsx
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import {
|
||||||
|
ChevronRight,
|
||||||
|
ChevronLeft,
|
||||||
|
MapPin,
|
||||||
|
CreditCard,
|
||||||
|
Truck,
|
||||||
|
ShieldCheck,
|
||||||
|
Calendar,
|
||||||
|
Wallet,
|
||||||
|
Clock,
|
||||||
|
ArrowRight,
|
||||||
|
CheckCircle2,
|
||||||
|
Heart,
|
||||||
|
PlusCircle
|
||||||
|
} from "lucide-react";
|
||||||
|
import { toPersian, cn } from "../lib/utils";
|
||||||
|
import { useCartStore } from "../store/cartStore";
|
||||||
|
import { usePetStore } from "../store/usePetStore";
|
||||||
|
import { useUserStore } from "../store/userStore";
|
||||||
|
import { PRODUCTS } from "../data/products";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export default function CheckoutPage({ onBack, onComplete }: { onBack: () => void; onComplete: () => void }) {
|
||||||
|
const { items, getTotal, getSubtotal, getDiscount, isSubscribed, charityDonation, setCharityDonation, addOrder, clearCart } = useCartStore();
|
||||||
|
const { getActivePet, updatePet } = usePetStore();
|
||||||
|
const { profile, updateProfile } = useUserStore();
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [useRoundUp, setUseRoundUp] = useState(false);
|
||||||
|
|
||||||
|
const subtotal = getSubtotal();
|
||||||
|
const roundedAmount = Math.ceil(subtotal / 10000) * 10000;
|
||||||
|
const roundUpDiff = roundedAmount - subtotal;
|
||||||
|
|
||||||
|
const handleCharityToggle = () => {
|
||||||
|
if (!useRoundUp) {
|
||||||
|
setCharityDonation(Math.max(roundUpDiff, Math.round(subtotal * 0.01)));
|
||||||
|
setUseRoundUp(true);
|
||||||
|
} else {
|
||||||
|
setCharityDonation(0);
|
||||||
|
setUseRoundUp(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinalize = () => {
|
||||||
|
setLoading(true);
|
||||||
|
const activePet = getActivePet();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// 1. Register the order
|
||||||
|
addOrder({
|
||||||
|
items: [...items],
|
||||||
|
total: getTotal(),
|
||||||
|
charityDonation: charityDonation,
|
||||||
|
petId: activePet?.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Update user charity total
|
||||||
|
if (charityDonation > 0) {
|
||||||
|
updateProfile({
|
||||||
|
charityDonationTotal: (profile.charityDonationTotal || 0) + charityDonation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update pet consumptions for the refill logic
|
||||||
|
if (activePet) {
|
||||||
|
const newConsumptions = [...(activePet.consumptions || [])];
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
const existing = newConsumptions.find(c => c.productId === item.product.id);
|
||||||
|
if (existing) {
|
||||||
|
existing.remaining += item.product.packageSize * item.quantity;
|
||||||
|
existing.packageSize = item.product.packageSize;
|
||||||
|
} else {
|
||||||
|
newConsumptions.push({
|
||||||
|
productId: item.product.id,
|
||||||
|
packageSize: item.product.packageSize,
|
||||||
|
remaining: item.product.packageSize * item.quantity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updatePet(activePet.id, { consumptions: newConsumptions });
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
clearCart();
|
||||||
|
onComplete();
|
||||||
|
toast.success("سفارش شما با موفقیت ثبت شد و به پرونده سلامت همدمتان اضافه شد!");
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-medical-gray-50 flex items-center justify-center p-6" dir="rtl">
|
||||||
|
<div className="text-center space-y-6">
|
||||||
|
<div className="w-24 h-24 bg-white rounded-full flex items-center justify-center mx-auto text-medical-gray-200">
|
||||||
|
<ShieldCheck className="w-12 h-12" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-black text-medical-gray-900 italic">سبد خرید شما خالی است</h2>
|
||||||
|
<button onClick={onBack} className="text-canina-blue font-black underline underline-offset-8">بازگشت به فروشگاه</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-medical-gray-50 py-12 px-6 font-vazir" dir="rtl">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
|
||||||
|
{/* Mobile Navigation & Breadcrumbs */}
|
||||||
|
<div className="lg:hidden mb-10 space-y-4">
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="flex items-center gap-2 text-medical-gray-500 font-bold hover:text-canina-blue transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
<span>بازگشت به سبد خرید</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest overflow-x-auto whitespace-nowrap pb-2">
|
||||||
|
<span className="cursor-pointer hover:text-canina-blue" onClick={onBack}>سبد خرید</span>
|
||||||
|
<ChevronLeft className="w-2.5 h-2.5" />
|
||||||
|
<span className="text-canina-blue">نهاییسازی سفارش</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Breadcrumbs (Hidden on Mobile) */}
|
||||||
|
<div className="hidden lg: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={onBack}>خانه</span>
|
||||||
|
<ChevronLeft className="w-2.5 h-2.5" />
|
||||||
|
<span className="text-canina-blue">درگاه پرداخت امن و نهاییسازی</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid lg:grid-cols-12 gap-12">
|
||||||
|
{/* Main Form */}
|
||||||
|
<div className="lg:col-span-8 space-y-8">
|
||||||
|
{/* Step 1: Shipping */}
|
||||||
|
<section className="bg-white rounded-[3rem] border border-medical-gray-200 p-10 overflow-hidden relative">
|
||||||
|
<div className="absolute top-0 right-0 w-3 h-full bg-canina-blue opacity-20" />
|
||||||
|
<div className="flex items-center gap-4 mb-10">
|
||||||
|
<div className="w-12 h-12 bg-medical-gray-900 text-white rounded-2xl flex items-center justify-center font-black">۱</div>
|
||||||
|
<h2 className="text-2xl font-black text-medical-gray-900 italic">اطلاعات ارسال</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">نام و نام خانوادگی</label>
|
||||||
|
<input type="text" className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 px-6 text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20" placeholder="مثلاً: پارسا آقایی" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">شماره تماس</label>
|
||||||
|
<input type="text" className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 px-6 text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20" placeholder="۰۹۱۲XXXXXXX" />
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-2 space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">آدرس دقیق پستی</label>
|
||||||
|
<textarea className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 px-6 text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20 h-32" placeholder="استان، شهر، خیابان..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Step 2: Payment */}
|
||||||
|
<section className="bg-white rounded-[3rem] border border-medical-gray-200 p-10 overflow-hidden relative">
|
||||||
|
<div className="absolute top-0 right-0 w-3 h-full bg-medical-gray-900 opacity-20" />
|
||||||
|
<div className="flex items-center gap-4 mb-10">
|
||||||
|
<div className="w-12 h-12 bg-medical-gray-900 text-white rounded-2xl flex items-center justify-center font-black">۲</div>
|
||||||
|
<h2 className="text-2xl font-black text-medical-gray-900 italic">شیوه پرداخت</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-4">
|
||||||
|
{[
|
||||||
|
{ id: 'online', label: 'پرداخت آنلاین', desc: 'کارتهای بانکی', icon: <CreditCard className="w-6 h-6" /> },
|
||||||
|
{ id: 'wallet', label: 'اعتبار حساب', desc: 'شارژ شده اختصاصی', icon: <Wallet className="w-6 h-6" /> },
|
||||||
|
{ id: 'cod', label: 'پرداخت در محل', desc: 'فقط تهران', icon: <Truck className="w-6 h-6" /> }
|
||||||
|
].map((pay) => (
|
||||||
|
<div
|
||||||
|
key={pay.id}
|
||||||
|
onClick={() => {
|
||||||
|
// TODO: [BACKEND_API] POST /api/payment/init | Payload: { method: string, amount: number } | Expected: { gatewayUrl: string } | Errors: [400]
|
||||||
|
}}
|
||||||
|
className="p-6 rounded-3xl border-2 border-medical-gray-100 hover:border-canina-blue cursor-pointer transition-all group"
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 bg-medical-gray-50 rounded-2xl flex items-center justify-center text-medical-gray-400 group-hover:text-canina-blue group-hover:bg-canina-blue/10 mb-4 transition-all">
|
||||||
|
{pay.icon}
|
||||||
|
</div>
|
||||||
|
<h4 className="font-black text-sm text-medical-gray-900 mb-1">{pay.label}</h4>
|
||||||
|
<p className="text-[10px] text-medical-gray-400 font-bold">{pay.desc}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Step 3: Charity Section */}
|
||||||
|
<section className="bg-white rounded-[3rem] border border-medical-gray-200 p-10 overflow-hidden relative">
|
||||||
|
<div className="absolute top-0 right-0 w-3 h-full bg-pink-500 opacity-20" />
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 bg-pink-500 text-white rounded-2xl flex items-center justify-center">
|
||||||
|
<Heart className="w-6 h-6 fill-white" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-black text-medical-gray-900 italic">ردپای مهربانی</h2>
|
||||||
|
<p className="text-xs font-bold text-pink-500 mt-1">سهم شما در حمایت از حیوانات بیپناه</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-pink-50 rounded-[2.5rem] p-8 border border-pink-100">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-6">
|
||||||
|
<div className="flex-1 min-w-[280px]">
|
||||||
|
<h4 className="text-lg font-black text-medical-gray-900 italic mb-2">رند کردن مبلغ و کمک به پناهگاه</h4>
|
||||||
|
<p className="text-sm text-medical-gray-600 leading-relaxed font-medium"> با انتخاب این گزینه، مابقی مبلغ تا ده هزار تومان بعدی یا ۱٪ از خرید شما (هر کدام بیشتر باشد) صرف تامین غذا و دارو برای حیوانات بی-سرپرست تحت حمایت کانینا میشود.</p>
|
||||||
|
<div className="mt-4 flex items-center gap-3 text-pink-600">
|
||||||
|
<ShieldCheck className="w-4 h-4" />
|
||||||
|
<span className="text-[10px] font-black uppercase tracking-widest">گزارش شفاف هزینهکرد در داشبورد شما</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleCharityToggle}
|
||||||
|
className={cn(
|
||||||
|
"w-20 h-20 rounded-full flex items-center justify-center transition-all shadow-xl hover:scale-110 active:scale-95",
|
||||||
|
useRoundUp ? "bg-pink-500 text-white shadow-pink-200" : "bg-white text-medical-gray-300 border-2 border-medical-gray-100"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PlusCircle className={cn("w-10 h-10", useRoundUp ? "rotate-45" : "rotate-0")} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{useRoundUp && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="mt-6 pt-6 border-t border-pink-100 flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<span className="text-sm font-black text-pink-600 italic">مبلغ اهدایی شما:</span>
|
||||||
|
<span className="text-lg font-black font-vazir text-pink-500">{toPersian(charityDonation.toLocaleString())} تومان</span>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-6 text-[10px] text-center text-medical-gray-400 font-bold">«با خرید این محصول، شما هم در درمان یک حیوان پناهگاهی سهیم شدید.»</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sidebar Summary */}
|
||||||
|
<div className="lg:col-span-4 space-y-8">
|
||||||
|
<div className="bg-medical-gray-900 rounded-[3rem] p-10 text-white shadow-2xl sticky top-32">
|
||||||
|
<h3 className="text-2xl font-black italic mb-8 border-b border-white/10 pb-4">خلاصه سفارش</h3>
|
||||||
|
|
||||||
|
<div className="space-y-6 mb-10 overflow-y-auto max-h-60 pr-2">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.product.id} className="flex justify-between items-start gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-black text-white/90 leading-tight">{item.product.name}</p>
|
||||||
|
<p className="text-[10px] text-white/40 font-bold mt-1">تعداد: {toPersian(item.quantity)} بسته</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-mono font-bold font-vazir">{toPersian((item.product.priceValue * item.quantity).toLocaleString())}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 pt-8 border-t border-white/10">
|
||||||
|
<div className="flex justify-between text-sm font-bold text-white/60">
|
||||||
|
<span>مجموع اقلام</span>
|
||||||
|
<span className="font-vazir">{toPersian(getSubtotal().toLocaleString())} تومان</span>
|
||||||
|
</div>
|
||||||
|
{isSubscribed && (
|
||||||
|
<div className="flex justify-between text-sm font-bold text-green-400">
|
||||||
|
<span>تخفیف اشتراک (۵٪-)</span>
|
||||||
|
<span className="font-vazir">{toPersian(getDiscount().toLocaleString())} تومان</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{charityDonation > 0 && (
|
||||||
|
<div className="flex justify-between text-sm font-bold text-pink-400">
|
||||||
|
<span>ردپای مهربانی</span>
|
||||||
|
<span className="font-vazir">{toPersian(charityDonation.toLocaleString())} تومان+</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between text-sm font-bold text-white/60">
|
||||||
|
<span>هزینه ارسال</span>
|
||||||
|
<span className="text-green-400 uppercase tracking-widest text-[10px] font-black">رایگان</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-end pt-4">
|
||||||
|
<span className="text-xl font-black italic">مبلغ نهایی</span>
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-3xl font-black text-canina-blue leading-none font-vazir">{toPersian(getTotal().toLocaleString())}</p>
|
||||||
|
<p className="text-[10px] text-white/30 font-bold mt-1">تومان</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Discount Code */}
|
||||||
|
<div className="mt-8 pt-8 border-t border-white/10">
|
||||||
|
<form
|
||||||
|
className="flex gap-2"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const code = (e.currentTarget.elements.namedItem('coupon') as HTMLInputElement).value;
|
||||||
|
if (useCartStore.getState().applyCoupon(code)) {
|
||||||
|
toast.success("تبریگ! کد تخفیف با موفقیت اعمال شد.");
|
||||||
|
} else {
|
||||||
|
toast.error("کد تخفیف معتبر نیست.");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name="coupon"
|
||||||
|
type="text"
|
||||||
|
placeholder="کد تخفیف (مثلاً: CANINA2024)"
|
||||||
|
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs focus:ring-1 focus:ring-canina-blue outline-none"
|
||||||
|
/>
|
||||||
|
<button type="submit" className="bg-white/10 px-4 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-white/20 transition-all">اعمال</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Refill Estimates */}
|
||||||
|
<div className="mt-10 p-6 bg-white/5 rounded-3xl border border-white/10 space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Clock className="w-5 h-5 text-canina-blue" />
|
||||||
|
<h4 className="text-xs font-black uppercase tracking-widest font-vazir">تخمین اتمام مصرف (هوش مصنوعی)</h4>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{items.map(item => {
|
||||||
|
const activePet = usePetStore.getState().getActivePet();
|
||||||
|
// Re-hydrate product to ensure methods like calculateDosage exist
|
||||||
|
const fullProduct = PRODUCTS.find(p => p.id === item.product.id) || item.product;
|
||||||
|
const dose = activePet ? fullProduct.calculateDosage(activePet.weight, activePet.age <= 1) : { quantity: 1 };
|
||||||
|
const days = Math.floor(fullProduct.packageSize / (dose.quantity || 1));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.product.id} className="flex items-center justify-between text-[10px] font-medium font-vazir">
|
||||||
|
<span className="text-white/40">{item.product.name}</span>
|
||||||
|
<span className="text-canina-blue">~ {toPersian(days)} روز دیگر</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleFinalize}
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-canina-blue text-white py-6 rounded-2xl font-black text-xl mt-10 hover:scale-[1.02] transition-all shadow-xl shadow-canina-blue/20 flex items-center justify-center gap-3 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<div className="w-6 h-6 border-4 border-white/20 border-t-white rounded-full animate-spin" />
|
||||||
|
در حال ثبت...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
پرداخت و تکمیل {step === 1 ? 'سفارش' : 'مرحله'}
|
||||||
|
<ArrowRight className="w-6 h-6 rotate-180" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="mt-6 flex items-center justify-center gap-2 text-[10px] font-black text-white/20 tracking-widest uppercase">
|
||||||
|
<ShieldCheck className="w-4 h-4" />
|
||||||
|
تضمین امنیت تراکنش بانکی
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
src/components/DeleteConfirmModal.tsx
Normal file
61
src/components/DeleteConfirmModal.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { Trash2, AlertTriangle, X } from "lucide-react";
|
||||||
|
|
||||||
|
interface DeleteConfirmModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeleteConfirmModal({ isOpen, onClose, onConfirm, title, message }: DeleteConfirmModalProps) {
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="fixed inset-0 z-[120] flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute inset-0 bg-medical-gray-900/60 backdrop-blur-md"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
className="bg-white w-full max-w-sm rounded-[2.5rem] relative z-10 shadow-2xl p-8 text-center font-vazir"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<div className="w-16 h-16 bg-red-50 rounded-2xl flex items-center justify-center text-red-500 mx-auto mb-6">
|
||||||
|
<Trash2 className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-xl font-black text-medical-gray-900 mb-2 italic">{title}</h3>
|
||||||
|
<p className="text-sm font-bold text-medical-gray-500 leading-relaxed mb-8">{message}</p>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 py-4 bg-medical-gray-50 text-medical-gray-400 rounded-2xl font-black text-sm hover:bg-medical-gray-100 transition-all"
|
||||||
|
>
|
||||||
|
خیر، بماند
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onConfirm();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
className="flex-1 py-4 bg-red-500 text-white rounded-2xl font-black text-sm hover:bg-red-600 transition-all shadow-lg shadow-red-500/20"
|
||||||
|
>
|
||||||
|
بله، حذف شود
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
71
src/components/ErrorBoundary.tsx
Normal file
71
src/components/ErrorBoundary.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
|
import { AlertTriangle, RefreshCcw } from 'lucide-react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends React.Component<Props, State> {
|
||||||
|
public state: State;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
hasError: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getDerivedStateFromError(_: Error): State {
|
||||||
|
return { hasError: true, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
public override componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
console.error('Uncaught error:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRetry = () => {
|
||||||
|
this.setState({ hasError: false, error: null });
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
public override render(): ReactNode {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-medical-gray-50 flex items-center justify-center p-6" dir="rtl">
|
||||||
|
<div className="max-w-md w-full bg-white rounded-[3rem] p-12 shadow-2xl text-center border border-medical-gray-100">
|
||||||
|
<div className="w-24 h-24 bg-rose-50 rounded-[2.5rem] flex items-center justify-center mx-auto mb-8">
|
||||||
|
<AlertTriangle className="w-12 h-12 text-rose-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-3xl font-black text-medical-gray-900 mb-4">متأسفانه خطایی رخ داده است</h1>
|
||||||
|
<p className="text-medical-gray-500 mb-10 font-medium leading-relaxed">
|
||||||
|
مشکلی در بارگذاری این بخش پیش آمده است. لطفاً برای ادامه دوباره تلاش کنید.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={this.handleRetry}
|
||||||
|
className="w-full bg-canina-blue text-white py-5 rounded-2xl font-black flex items-center justify-center gap-3 hover:scale-105 transition-all shadow-xl shadow-canina-blue/20"
|
||||||
|
>
|
||||||
|
<RefreshCcw className="w-5 h-5" />
|
||||||
|
تلاش مجدد
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="mt-8 pt-8 border-t border-medical-gray-50 flex items-center justify-center gap-2">
|
||||||
|
<div className="w-8 h-8 bg-canina-blue rounded-xl flex items-center justify-center text-white font-bold text-xs italic">C</div>
|
||||||
|
<span className="text-[10px] font-black text-medical-gray-300 uppercase tracking-widest">Canina Pharma Support</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/components/ErrorPages.tsx
Normal file
59
src/components/ErrorPages.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FileQuestion, AlertCircle, Home, RefreshCw } from 'lucide-react';
|
||||||
|
|
||||||
|
export const NotFoundPage = ({ onGoHome }: { onGoHome: () => void }) => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[80vh] flex items-center justify-center p-6" dir="rtl">
|
||||||
|
<div className="max-w-lg w-full text-center">
|
||||||
|
<div className="relative mb-12">
|
||||||
|
<div className="text-[15rem] font-black text-medical-gray-100 leading-none select-none">۴۰۴</div>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="w-32 h-32 bg-white rounded-[3rem] shadow-2xl flex items-center justify-center">
|
||||||
|
<FileQuestion className="w-16 h-16 text-canina-blue" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-4xl font-black text-medical-gray-900 mb-4">صفحه پیدا نشد!</h1>
|
||||||
|
<p className="text-medical-gray-500 mb-12 font-bold text-lg">
|
||||||
|
متأسفانه صفحهای که به دنبال آن هستید وجود ندارد یا جابجا شده است.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onGoHome}
|
||||||
|
className="bg-medical-gray-900 text-white px-10 py-5 rounded-2xl font-black flex items-center justify-center gap-3 mx-auto hover:bg-canina-blue transition-all shadow-2xl shadow-medical-gray-900/10"
|
||||||
|
>
|
||||||
|
<Home className="w-5 h-5" />
|
||||||
|
بازگشت به خانه
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ServerErrorPage = ({ onRetry }: { onRetry: () => void }) => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[80vh] flex items-center justify-center p-6" dir="rtl">
|
||||||
|
<div className="max-w-lg w-full text-center">
|
||||||
|
<div className="w-24 h-24 bg-rose-50 rounded-[2.5rem] flex items-center justify-center mx-auto mb-8">
|
||||||
|
<AlertCircle className="w-12 h-12 text-rose-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-4xl font-black text-medical-gray-900 mb-4">خطای داخلی سیستم</h1>
|
||||||
|
<p className="text-medical-gray-500 mb-12 font-bold text-lg">
|
||||||
|
ارتباط با دیتابیس کانینا با مشکل مواجه شده است. در حال بررسی موضوع هستیم.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<button
|
||||||
|
onClick={onRetry}
|
||||||
|
className="bg-canina-blue text-white px-10 py-5 rounded-2xl font-black flex items-center justify-center gap-3 hover:scale-105 transition-all shadow-xl shadow-canina-blue/20"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-5 h-5" />
|
||||||
|
تلاش مجدد
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
147
src/components/FeaturedProducts.tsx
Normal file
147
src/components/FeaturedProducts.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { useState, useEffect, useMemo } from "react";
|
||||||
|
import { Product } from "../data/products";
|
||||||
|
import { usePetStore } from "../store/usePetStore";
|
||||||
|
import { productService } from "../services/productService";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { Eye, ShoppingCart, ShieldCheck, Heart, AlertCircle, Sparkles } from "lucide-react";
|
||||||
|
import SafeImage from "./SafeImage";
|
||||||
|
import { ProductCardSkeleton } from "./Skeleton";
|
||||||
|
|
||||||
|
function ProductCard({ product, onClick }: { product: Product; key?: string | number; onClick: (p: Product) => void }) {
|
||||||
|
const { getActivePet } = usePetStore();
|
||||||
|
const activePet = getActivePet();
|
||||||
|
|
||||||
|
const compatibility = useMemo(() => {
|
||||||
|
if (!activePet) return null;
|
||||||
|
const sameSpecies = product.suitableFor === activePet.type || product.suitableFor === "هر دو";
|
||||||
|
const helpsSymptom = (activePet.medicalConditions || []).some(s => product.symptoms.includes(s));
|
||||||
|
if (!sameSpecies) return { type: 'alert', text: `مخصوص ${product.suitableFor}` };
|
||||||
|
if (helpsSymptom) return { type: 'success', text: `توصیه شده برای ${activePet.name}` };
|
||||||
|
return { type: 'neutral', text: `مناسب برای ${activePet.name}` };
|
||||||
|
}, [product, activePet]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
onClick={() => onClick(product)}
|
||||||
|
className="group bg-white rounded-3xl border border-medical-gray-200 overflow-hidden hover:shadow-2xl hover:shadow-canina-blue/10 transition-all duration-500 flex flex-col h-full cursor-pointer relative"
|
||||||
|
>
|
||||||
|
{compatibility && (
|
||||||
|
<div className={`absolute top-16 right-4 z-10 text-[8px] font-black uppercase tracking-widest px-3 py-1 rounded-full shadow-md flex items-center gap-1 ${
|
||||||
|
compatibility.type === 'alert' ? 'bg-amber-100 text-amber-700' :
|
||||||
|
compatibility.type === 'success' ? 'bg-green-100 text-green-700' : 'bg-medical-gray-100 text-medical-gray-600'
|
||||||
|
}`}>
|
||||||
|
{compatibility.type === 'alert' ? <AlertCircle className="w-2.5 h-2.5" /> : compatibility.type === 'success' ? <Heart className="w-2.5 h-2.5" /> : <Sparkles className="w-2.5 h-2.5" />}
|
||||||
|
{compatibility.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Product Image Area */}
|
||||||
|
<div className="relative aspect-square bg-medical-gray-50 p-10 flex items-center justify-center overflow-hidden">
|
||||||
|
<SafeImage
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-full h-full object-contain group-hover:scale-105 transition-transform duration-500 drop-shadow-2xl"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-4 right-4 px-3 py-1 bg-white/80 backdrop-blur-md rounded-full border border-medical-gray-200 text-[10px] font-bold text-medical-gray-500 uppercase tracking-widest">
|
||||||
|
{product.category}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hover Actions */}
|
||||||
|
<div className="absolute inset-0 bg-canina-blue/60 opacity-0 group-hover:opacity-100 backdrop-blur-sm transition-all duration-300 flex items-center justify-center gap-4">
|
||||||
|
<div className="w-12 h-12 rounded-full bg-white text-canina-blue flex items-center justify-center hover:scale-110 transition-transform shadow-lg">
|
||||||
|
<Eye className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div className="w-12 h-12 rounded-full bg-canina-blue text-white flex items-center justify-center hover:scale-110 transition-transform shadow-lg border border-white/20">
|
||||||
|
<ShoppingCart className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
<div className="p-8 flex flex-col flex-1">
|
||||||
|
<div className="flex items-start justify-between mb-4 gap-2">
|
||||||
|
<h3 className="text-xl font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors leading-tight">
|
||||||
|
{product.name}
|
||||||
|
</h3>
|
||||||
|
<ShieldCheck className="w-6 h-6 text-canina-blue flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-medical-gray-500 leading-relaxed mb-6 flex-1">
|
||||||
|
{product.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4 pt-4 border-t border-medical-gray-100">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-1 h-1 rounded-full bg-canina-blue" />
|
||||||
|
<span className="text-xs font-semibold text-medical-gray-700 italic">{product.benefits}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-xl font-black text-medical-gray-900 font-vazir">{product.price}</div>
|
||||||
|
<button className="text-xs font-black text-canina-blue uppercase tracking-widest border-b-2 border-canina-blue pb-1 hover:text-medical-gray-900 hover:border-medical-gray-900 transition-all">
|
||||||
|
مشاهده جزییات
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FeaturedProducts({ onProductClick, onShopNavigate }: { onProductClick: (p: Product) => void, onShopNavigate: () => void }) {
|
||||||
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchFeatured = async () => {
|
||||||
|
try {
|
||||||
|
const data = await productService.getFeaturedProducts();
|
||||||
|
setProducts(data);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchFeatured();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-medical-gray-50">
|
||||||
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
|
<div className="flex flex-col lg:flex-row items-end justify-between mb-16 gap-6">
|
||||||
|
<div className="lg:w-2/3">
|
||||||
|
<div className="text-canina-blue text-xs font-bold uppercase tracking-[0.2em] mb-4 font-vazir">بیشترین انتخاب توسط دامپزشکان</div>
|
||||||
|
<h2 className="text-3xl lg:text-5xl font-black text-medical-gray-900 leading-[1.2]">
|
||||||
|
محصولات برگزیده و راهکارهای <br />
|
||||||
|
<span className="italic text-canina-blue">درمان تخصصی</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="lg:w-1/3 text-left lg:text-right">
|
||||||
|
<p className="text-medical-gray-500 text-sm leading-relaxed mb-4">
|
||||||
|
محصولات دارویی کانینا با استفاده از دانش پیشرفته بیوتکنولوژی و مواد اولیه ارگانیک نایاب فرآوری شدهاند.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={onShopNavigate}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
مشاهده تمامی محصولات
|
||||||
|
<span className="text-lg">←</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
|
||||||
|
{isLoading ? (
|
||||||
|
Array.from({ length: 3 }).map((_, i) => <ProductCardSkeleton key={i} />)
|
||||||
|
) : (
|
||||||
|
products.map((p) => (
|
||||||
|
<ProductCard key={p.id} onClick={onProductClick} product={p} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
127
src/components/Footer.tsx
Normal file
127
src/components/Footer.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Phone, Mail, MapPin, Instagram, Youtube, Linkedin, ShieldCheck, Globe } from "lucide-react";
|
||||||
|
|
||||||
|
export default function Footer({ onNavigate, onShopNavigate, onB2BOpen }: {
|
||||||
|
onNavigate: (v: any) => void,
|
||||||
|
onShopNavigate: (c?: string) => void,
|
||||||
|
onB2BOpen?: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<footer className="bg-medical-gray-900 text-white pt-24 pb-12 font-vazir" dir="rtl">
|
||||||
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-16 mb-20">
|
||||||
|
|
||||||
|
{/* Brand Presence */}
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="flex items-center gap-4 cursor-pointer group" onClick={() => onNavigate("home")}>
|
||||||
|
<div className="w-16 h-16 bg-white rounded-2xl flex items-center justify-center p-2 group-hover:scale-105 transition-transform">
|
||||||
|
<img src="https://canina.de/media/fb/d5/47/1683116813/Logo.png" alt="Canina Logo" className="w-full h-auto" referrerPolicy="no-referrer" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-black italic tracking-tighter">کانینا ایران</h3>
|
||||||
|
<p className="text-[10px] text-white/40 font-bold uppercase tracking-widest">نماینده رسمی در ایران</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-white/60 leading-relaxed font-medium">
|
||||||
|
تدارک هوشمندانه سلامت برای همراهان وفادار شما. واردکننده انحصاری مکملهای درمانی با گرید دارویی اختصاصی از آلمان با سابقه ۴۰ سال نوآوری.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{[Instagram, Youtube, Linkedin, Globe].map((Icon, i) => (
|
||||||
|
<a key={i} href="#" className="w-10 h-10 bg-white/5 rounded-xl flex items-center justify-center hover:bg-canina-blue hover:text-white transition-all text-white/40">
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Links */}
|
||||||
|
<div className="space-y-8">
|
||||||
|
<h4 className="text-lg font-black text-canina-blue italic underline decoration-white/20 underline-offset-8">دسترسی سریع</h4>
|
||||||
|
<ul className="space-y-4 text-sm font-bold text-white/60">
|
||||||
|
<li onClick={() => onShopNavigate()} className="hover:text-white hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-canina-blue group-hover:w-4 transition-all" />
|
||||||
|
محصولات تخصصی ۲۰۲۴
|
||||||
|
</li>
|
||||||
|
<li onClick={() => onNavigate("blog")} className="hover:text-white hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-canina-blue group-hover:w-4 transition-all" />
|
||||||
|
مجله سلامت پت (وبلاگ)
|
||||||
|
</li>
|
||||||
|
<li onClick={() => onNavigate("profile")} className="hover:text-white hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-canina-blue group-hover:w-4 transition-all" />
|
||||||
|
شناسنامه پتها
|
||||||
|
</li>
|
||||||
|
<li onClick={() => onNavigate("wiki")} className="hover:text-white hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-canina-blue group-hover:w-4 transition-all" />
|
||||||
|
دانشنامه ترکیبات
|
||||||
|
</li>
|
||||||
|
<li onClick={onB2BOpen} className="hover:text-canina-blue hover:translate-x-[-4px] transition-all cursor-pointer flex items-center gap-2 group text-blue-300">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-canina-blue group-hover:w-4 transition-all" />
|
||||||
|
پنل سفارش عمده (B2B)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Location & Contact */}
|
||||||
|
<div className="space-y-8 lg:col-span-2">
|
||||||
|
<h4 className="text-lg font-black text-canina-blue italic underline decoration-white/20 underline-offset-8">اطلاعات تماس نمایندگی</h4>
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex gap-4 group">
|
||||||
|
<div className="w-12 h-12 bg-white/5 rounded-2xl flex items-center justify-center text-canina-blue group-hover:scale-110 transition-transform flex-shrink-0">
|
||||||
|
<Phone className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-white/40 font-black uppercase mb-1">خط ویژه فروش</p>
|
||||||
|
<p className="text-lg font-black tracking-widest text-left" dir="ltr">۰۲۱-۸۸۸۸ ۴۴۴۴</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 group">
|
||||||
|
<div className="w-12 h-12 bg-white/5 rounded-2xl flex items-center justify-center text-canina-blue group-hover:scale-110 transition-transform flex-shrink-0">
|
||||||
|
<Mail className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-white/40 font-black uppercase mb-1">مکاتبات رسمی</p>
|
||||||
|
<p className="text-sm font-bold">info@canina-iran.com</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 group">
|
||||||
|
<div className="w-12 h-12 bg-white/5 rounded-2xl flex items-center justify-center text-canina-blue group-hover:scale-110 transition-transform flex-shrink-0">
|
||||||
|
<MapPin className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-white/40 font-black uppercase mb-1">دفتر مرکزی</p>
|
||||||
|
<p className="text-sm font-bold leading-relaxed">تهران، جردن، خیابان سعیدی، ساختمان کانینا، طبقه ۵، واحد ۱۹</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-10 p-6 bg-white/5 rounded-[2rem] border border-white/10 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-10 h-10 bg-green-500/20 text-green-500 rounded-full flex items-center justify-center">
|
||||||
|
<ShieldCheck className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="text-sm font-black">اصالت کالا تضمین میشود</h5>
|
||||||
|
<p className="text-[10px] text-white/40 font-bold">تمامی محصولات دارای لیبل اصالت وزارت بهداشت و QR Code اختصاصی هستند.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-black italic text-white/20">اصالت آلمانی کانینا</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-12 border-t border-white/5 flex flex-col md:flex-row items-center justify-between gap-6 text-[10px] font-black text-white/20 uppercase tracking-[0.3em]">
|
||||||
|
<div>© ۲۰۲۶ کانینا ایران. تمامی حقوق محفوظ است.</div>
|
||||||
|
<div className="flex gap-8 font-vazir">
|
||||||
|
<button onClick={() => onNavigate("home")} className="hover:text-white transition-colors">درباره ما</button>
|
||||||
|
<button onClick={() => onNavigate("home")} className="hover:text-white transition-colors">تماس با ما</button>
|
||||||
|
<button className="hover:text-white transition-colors">حریم خصوصی</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
363
src/components/Header.tsx
Normal file
363
src/components/Header.tsx
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
import { Search, ChevronDown, Menu, X, Pill, ShieldCheck, HeartPulse, Sparkles, ShoppingBag, User, PlusCircle, Check, Building2, LogIn, Wallet, LogOut, MapPin, FileHeart, Dog, Cat } from "lucide-react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { useCartStore } from "../store/cartStore";
|
||||||
|
import { usePetStore } from "../store/usePetStore";
|
||||||
|
import { useUserStore } from "../store/userStore";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import HeaderButton from "./HeaderButton";
|
||||||
|
import AuthModal from "./AuthModal";
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
const MENU_ITEMS = [
|
||||||
|
{
|
||||||
|
id: "joints",
|
||||||
|
title: "مفاصل و استخوان",
|
||||||
|
icon: <Pill className="w-5 h-5" />,
|
||||||
|
solutions: ["آرتروز سگهای پیر", "رشد استخوانی تولهسگ", "تقویت رباط و تاندون"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "immune",
|
||||||
|
title: "تقویت سیستم ایمنی و گوارش",
|
||||||
|
icon: <ShieldCheck className="w-5 h-5" />,
|
||||||
|
solutions: ["پیشگیری از بیماری", "رفع اسهال و یبوست", "پروبیوتیکها"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "energy",
|
||||||
|
title: "ویتامینها و انرژیبخشها",
|
||||||
|
icon: <HeartPulse className="w-5 h-5" />,
|
||||||
|
solutions: ["مکملهای مولتیویتامین", "افزایش اشتها", "رشد و بلوغ"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "special-care",
|
||||||
|
title: "مراقبتهای ویژه (پوست، دندان و چشم)",
|
||||||
|
icon: <Sparkles className="w-5 h-5" />,
|
||||||
|
solutions: ["سلامت پوست و مفاصل", "رفع جرم دندان", "شستشوی چشم"]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Header({
|
||||||
|
onNavigate,
|
||||||
|
onShopNavigate,
|
||||||
|
currentView,
|
||||||
|
onCartOpen,
|
||||||
|
onSearch,
|
||||||
|
onB2BOpen
|
||||||
|
}: {
|
||||||
|
onNavigate: (v: any) => void,
|
||||||
|
onShopNavigate: (c?: string, s?: string) => void,
|
||||||
|
currentView: string,
|
||||||
|
onCartOpen: () => void,
|
||||||
|
onSearch: (q: string) => void,
|
||||||
|
onB2BOpen: () => void
|
||||||
|
}) {
|
||||||
|
const [isMegaMenuOpen, setIsMegaMenuOpen] = useState(false);
|
||||||
|
const [isSearchFocused, setIsSearchFocused] = useState(false);
|
||||||
|
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
|
||||||
|
const [isPetSwitcherOpen, setIsPetSwitcherOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
const petMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { getTotalItems } = useCartStore();
|
||||||
|
const { pets, activePetId, setActivePet, getActivePet } = usePetStore();
|
||||||
|
const { role, isLoggedIn, logout, profile } = useUserStore();
|
||||||
|
const activePet = getActivePet();
|
||||||
|
|
||||||
|
// Click Outside logic
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (petMenuRef.current && !petMenuRef.current.contains(event.target as Node)) {
|
||||||
|
setIsPetSwitcherOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (searchQuery.trim()) {
|
||||||
|
onSearch(searchQuery);
|
||||||
|
setIsSearchFocused(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AuthModal isOpen={isAuthModalOpen} onClose={() => setIsAuthModalOpen(false)} />
|
||||||
|
|
||||||
|
<header className="sticky top-0 z-50 bg-white/95 backdrop-blur-md border-b border-medical-gray-100 shadow-sm">
|
||||||
|
<div className="max-w-7xl h-24 flex items-center justify-between gap-8" dir="rtl">
|
||||||
|
{/* Logo Section */}
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 flex items-center gap-3 cursor-pointer group pr-2"
|
||||||
|
onClick={() => onNavigate("home")}
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 bg-canina-blue rounded-2xl flex items-center justify-center text-white font-bold text-2xl group-hover:bg-medical-gray-900 transition-all shadow-xl shadow-canina-blue/20 italic">C</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-canina-blue font-black text-2xl tracking-tighter leading-none italic font-inter">Canina <span className="text-sm not-italic font-medium border-l-2 border-medical-gray-100 pl-2 ml-2 font-vazir">ایران</span></span>
|
||||||
|
<span className="text-[10px] text-medical-gray-400 font-bold uppercase tracking-widest leading-none mt-1.5 font-vazir">نماینده رسمی Canina Pharma GmbH آلمان</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Items */}
|
||||||
|
<nav className="hidden xl:flex items-center gap-1 flex-grow justify-center h-full">
|
||||||
|
<div
|
||||||
|
className="relative h-full flex items-center group"
|
||||||
|
onMouseEnter={() => setIsMegaMenuOpen(true)}
|
||||||
|
onMouseLeave={() => setIsMegaMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<button className={`flex items-center gap-1.5 px-3 py-2 rounded-xl text-sm font-black transition-all whitespace-nowrap ${isMegaMenuOpen ? 'bg-medical-gray-50 text-canina-blue' : 'text-medical-gray-600 hover:bg-medical-gray-50 hover:text-canina-blue'} font-vazir`}>
|
||||||
|
راهکارهای درمانی
|
||||||
|
<ChevronDown className={`w-4 h-4 transition-transform duration-300 ${isMegaMenuOpen ? 'rotate-180' : ''}`} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{isMegaMenuOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 15, scale: 0.98 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 15, scale: 0.98 }}
|
||||||
|
className="absolute top-[80%] right-0 w-[640px] bg-white border border-medical-gray-100 shadow-2xl rounded-[2.5rem] p-10 grid grid-cols-2 gap-10 z-50 pointer-events-auto"
|
||||||
|
>
|
||||||
|
{MENU_ITEMS.map((item, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="group/item cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
onShopNavigate(item.id);
|
||||||
|
setIsMegaMenuOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<div className="p-3 bg-medical-gray-50 rounded-2xl text-canina-blue group-hover/item:bg-canina-blue group-hover/item:text-white transition-all shadow-sm">
|
||||||
|
{item.icon}
|
||||||
|
</div>
|
||||||
|
<h4 className="font-black text-medical-gray-900 text-sm font-vazir whitespace-nowrap">{item.title}</h4>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2.5 border-r-2 border-medical-gray-50 pr-4">
|
||||||
|
{item.solutions.map((sol, sIdx) => (
|
||||||
|
<li
|
||||||
|
key={sIdx}
|
||||||
|
className="text-[12px] text-medical-gray-400 hover:text-canina-blue hover:translate-x-[-4px] transition-all font-bold cursor-pointer font-vazir whitespace-nowrap"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onShopNavigate(item.id, sol);
|
||||||
|
setIsMegaMenuOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sol}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{[
|
||||||
|
{ id: 'shop', label: 'محصولات تخصصی', action: () => onShopNavigate() },
|
||||||
|
{ id: 'wiki', label: 'دانشنامه علمی', action: () => onNavigate('wiki') },
|
||||||
|
{ id: 'blog', label: 'مجله سلامت پت', action: () => onNavigate('blog') },
|
||||||
|
{ id: 'profile', label: 'شناسنامه پتها', action: () => onNavigate({ view: 'profile', subview: 'index' }) }
|
||||||
|
].map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={item.action}
|
||||||
|
className={`px-3 py-2 rounded-xl text-sm font-black transition-all font-vazir whitespace-nowrap ${currentView === item.id ? 'bg-canina-blue/5 text-canina-blue' : 'text-medical-gray-600 hover:bg-medical-gray-50 hover:text-canina-blue'}`}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Action Area */}
|
||||||
|
<div className="flex items-center gap-4 flex-shrink-0 justify-end h-16 pl-2">
|
||||||
|
|
||||||
|
{/* Elastic Search */}
|
||||||
|
<div className="relative flex items-center justify-end z-20">
|
||||||
|
<AnimatePresence>
|
||||||
|
{isSearchFocused && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ width: 0, opacity: 0 }}
|
||||||
|
animate={{ width: 350, opacity: 1 }}
|
||||||
|
exit={{ width: 0, opacity: 0 }}
|
||||||
|
transition={{ type: "spring", damping: 20, stiffness: 200 }}
|
||||||
|
className="absolute right-0 top-1/2 -translate-y-1/2 bg-white border-2 border-canina-blue rounded-2xl shadow-xl overflow-hidden h-12 flex items-center pr-10"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSearch} className="w-full flex items-center">
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
|
onBlur={() => !searchQuery && setIsSearchFocused(false)}
|
||||||
|
placeholder="جستجوی محصولات، مقالات سلامت و..."
|
||||||
|
className="w-full bg-transparent px-4 py-2 text-[12px] font-bold outline-none font-vazir text-medical-gray-900"
|
||||||
|
/>
|
||||||
|
<button type="submit" className="px-4 py-2 bg-canina-blue text-white text-[10px] font-black hover:bg-medical-gray-900 transition-colors">
|
||||||
|
جستجو
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<HeaderButton
|
||||||
|
variant={isSearchFocused ? "primary" : "secondary"}
|
||||||
|
className={cn("w-12 h-12 transition-all duration-300", isSearchFocused ? "z-30 bg-canina-blue text-white" : "")}
|
||||||
|
onClick={() => setIsSearchFocused(!isSearchFocused)}
|
||||||
|
>
|
||||||
|
<Search className="w-5 h-5" />
|
||||||
|
</HeaderButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 items-center h-12">
|
||||||
|
{isLoggedIn ? (
|
||||||
|
<div className="flex items-center h-full gap-2 p-1.5 bg-medical-gray-50/50 border border-medical-gray-100 rounded-3xl">
|
||||||
|
{/* User Name (Direct link to dashboard) */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2.5 px-4 py-2 hover:bg-white rounded-2xl cursor-pointer transition-all border border-transparent hover:border-medical-gray-200"
|
||||||
|
onClick={() => onNavigate("user-dashboard")}
|
||||||
|
>
|
||||||
|
<User className="w-5 h-5 text-canina-blue" />
|
||||||
|
<span className="text-[14px] font-black font-vazir text-medical-gray-900 leading-none">
|
||||||
|
{profile.firstName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Separator Line */}
|
||||||
|
<div className="w-[1.5px] h-5 bg-medical-gray-200" />
|
||||||
|
|
||||||
|
{/* Pet Name & Switcher */}
|
||||||
|
<div className="relative h-full flex items-center" ref={petMenuRef}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2.5 px-4 py-2 rounded-2xl transition-all cursor-pointer",
|
||||||
|
isPetSwitcherOpen ? "bg-canina-blue text-white shadow-lg shadow-canina-blue/20" : "text-canina-blue hover:bg-white"
|
||||||
|
)}
|
||||||
|
onClick={() => setIsPetSwitcherOpen(!isPetSwitcherOpen)}
|
||||||
|
>
|
||||||
|
{activePet?.type === "گربه" ? (
|
||||||
|
<Cat className={cn("w-5 h-5", isPetSwitcherOpen ? "text-white" : "text-canina-blue/60")} />
|
||||||
|
) : (
|
||||||
|
<Dog className={cn("w-5 h-5", isPetSwitcherOpen ? "text-white" : "text-canina-blue/60")} />
|
||||||
|
)}
|
||||||
|
<span className={cn("text-[14px] font-black font-vazir leading-none", isPetSwitcherOpen ? "text-white" : "text-canina-blue")}>
|
||||||
|
{activePet?.name || "بدون پت"}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className={cn("w-4 h-4 transition-transform", isPetSwitcherOpen ? "rotate-180" : "")} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{isPetSwitcherOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
|
className="absolute top-[125%] left-0 w-72 bg-white border border-medical-gray-100 shadow-2xl rounded-[1.5rem] p-5 z-50 pointer-events-auto"
|
||||||
|
>
|
||||||
|
<div className="text-[9px] font-black text-medical-gray-400 uppercase tracking-[0.2em] mb-4 pr-1">تغییر پت یا مدیریت حساب</div>
|
||||||
|
<div className="space-y-1 mb-4">
|
||||||
|
{pets.map(pet => (
|
||||||
|
<div
|
||||||
|
key={pet.id}
|
||||||
|
className={cn(
|
||||||
|
"w-full flex items-center justify-between p-3 rounded-xl transition-all group/item",
|
||||||
|
activePetId === pet.id ? "bg-canina-blue/5 text-canina-blue" : "hover:bg-medical-gray-50 text-medical-gray-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-3 flex-1 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setActivePet(pet.id);
|
||||||
|
setIsPetSwitcherOpen(false);
|
||||||
|
toast.success(`پت فعال به ${pet.name} تغییر یافت`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={cn(
|
||||||
|
"w-8 h-8 rounded-lg flex items-center justify-center text-[11px] font-black transition-colors",
|
||||||
|
activePetId === pet.id ? "bg-canina-blue text-white" : "bg-medical-gray-100 text-medical-gray-400 group-hover/item:bg-medical-gray-200"
|
||||||
|
)}>
|
||||||
|
{pet.name[0]}
|
||||||
|
</div>
|
||||||
|
<span className="text-[13px] font-bold font-vazir">{pet.name}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{activePetId === pet.id && <Check className="w-4 h-4 ml-2" />}
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setActivePet(pet.id);
|
||||||
|
onNavigate({ view: "profile", subview: "detail" });
|
||||||
|
setIsPetSwitcherOpen(false);
|
||||||
|
}}
|
||||||
|
className="p-1.5 rounded-lg hover:bg-canina-blue hover:text-white transition-all text-medical-gray-300 hover:shadow-lg"
|
||||||
|
title="مشاهده شناسنامه سلامت"
|
||||||
|
>
|
||||||
|
<FileHeart className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-medical-gray-100 space-y-1">
|
||||||
|
<button
|
||||||
|
onClick={() => { onNavigate({ view: "profile", subview: "add" }); setIsPetSwitcherOpen(false); }}
|
||||||
|
className="w-full flex items-center gap-3 p-3 text-canina-blue hover:bg-canina-blue/5 rounded-xl transition-all text-[12px] font-black font-vazir"
|
||||||
|
>
|
||||||
|
<PlusCircle className="w-5 h-5" />
|
||||||
|
ثبت همدم (Pet) جدید
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { onNavigate({ view: "user-dashboard" }); setIsPetSwitcherOpen(false); }}
|
||||||
|
className="w-full flex items-center gap-3 p-3 text-medical-gray-600 hover:bg-medical-gray-50 rounded-xl transition-all text-[12px] font-bold font-vazir"
|
||||||
|
>
|
||||||
|
<Wallet className="w-5 h-5" />
|
||||||
|
کیف پول و سفارشات
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { logout(); setIsPetSwitcherOpen(false); }}
|
||||||
|
className="w-full flex items-center gap-3 p-3 text-red-500 hover:bg-red-50 rounded-xl transition-all text-[12px] font-bold font-vazir mt-1"
|
||||||
|
>
|
||||||
|
<LogOut className="w-5 h-5" />
|
||||||
|
خروج از حساب کانینا
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<HeaderButton
|
||||||
|
variant="outline"
|
||||||
|
className="px-6 h-12 text-sm font-black"
|
||||||
|
onClick={() => setIsAuthModalOpen(true)}
|
||||||
|
icon={<LogIn className="w-5 h-5" />}
|
||||||
|
>
|
||||||
|
ورود / ثبتنام
|
||||||
|
</HeaderButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<HeaderButton
|
||||||
|
variant="primary"
|
||||||
|
className="w-16 h-12"
|
||||||
|
onClick={onCartOpen}
|
||||||
|
badge={getTotalItems()}
|
||||||
|
icon={<ShoppingBag className="w-6 h-6" />}
|
||||||
|
>
|
||||||
|
<div className="text-[8px] font-black tracking-tight whitespace-nowrap mt-0.5">سبد خرید</div>
|
||||||
|
</HeaderButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/components/HeaderButton.tsx
Normal file
52
src/components/HeaderButton.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { toPersian } from "../lib/utils";
|
||||||
|
|
||||||
|
interface HeaderButtonProps {
|
||||||
|
onClick?: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
variant?: "primary" | "secondary" | "outline" | "ghost";
|
||||||
|
className?: string;
|
||||||
|
badge?: number;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HeaderButton({
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
variant = "primary",
|
||||||
|
className = "",
|
||||||
|
badge,
|
||||||
|
icon
|
||||||
|
}: HeaderButtonProps) {
|
||||||
|
const baseStyles = "relative flex flex-col items-center justify-center rounded-xl transition-all duration-300 font-vazir select-none cursor-pointer overflow-visible";
|
||||||
|
|
||||||
|
const variants = {
|
||||||
|
primary: "bg-canina-blue text-white hover:bg-medical-gray-900 shadow-lg shadow-canina-blue/20",
|
||||||
|
secondary: "bg-medical-gray-50 text-medical-gray-600 hover:bg-medical-gray-100",
|
||||||
|
outline: "border-2 border-medical-gray-100 text-medical-gray-700 hover:border-canina-blue hover:text-canina-blue",
|
||||||
|
ghost: "text-medical-gray-600 hover:bg-medical-gray-50 hover:text-canina-blue"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ y: -2 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={onClick}
|
||||||
|
className={`${baseStyles} ${variants[variant]} ${className}`}
|
||||||
|
>
|
||||||
|
{icon && <div className="mb-0.5">{icon}</div>}
|
||||||
|
<div className="whitespace-nowrap">{children}</div>
|
||||||
|
|
||||||
|
{badge !== undefined && badge > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
className="absolute -top-1 -left-1 w-5 h-5 bg-red-500 text-white rounded-full border-2 border-white text-[8px] font-black flex items-center justify-center font-vazir"
|
||||||
|
>
|
||||||
|
{toPersian(badge.toString())}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
|
}
|
||||||
184
src/components/Hero.tsx
Normal file
184
src/components/Hero.tsx
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import { motion, useMotionValue, useTransform, animate } from "motion/react";
|
||||||
|
import { ChevronLeft } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toPersian } from "../lib/utils";
|
||||||
|
|
||||||
|
function StatCounter({ target }: { target: number }) {
|
||||||
|
const [displayValue, setDisplayValue] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const controls = animate(0, target, {
|
||||||
|
duration: 3,
|
||||||
|
ease: [0.16, 1, 0.3, 1], // Custom cubic-bezier for a more polished feel
|
||||||
|
onUpdate: (latest) => setDisplayValue(Math.round(latest))
|
||||||
|
});
|
||||||
|
return controls.stop;
|
||||||
|
}, [target]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ duration: 2.5, ease: "easeOut" }}
|
||||||
|
className="text-4xl lg:text-6xl font-black text-canina-blue font-vazir origin-right flex items-center"
|
||||||
|
>
|
||||||
|
<motion.span
|
||||||
|
animate={{ scale: [1, 1.1, 1] }}
|
||||||
|
transition={{ duration: 3, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
{toPersian(displayValue)}+
|
||||||
|
</motion.span>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Typewriter({ text }: { text: string }) {
|
||||||
|
const [displayText, setDisplayText] = useState("");
|
||||||
|
useEffect(() => {
|
||||||
|
let i = 0;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setDisplayText(text.slice(0, i));
|
||||||
|
i++;
|
||||||
|
if (i > text.length) clearInterval(interval);
|
||||||
|
}, 150);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-4xl lg:text-6xl font-black text-canina-blue font-vazir flex flex-row-reverse items-center">
|
||||||
|
<span className="relative">
|
||||||
|
{toPersian(displayText)}
|
||||||
|
<motion.span
|
||||||
|
animate={{ opacity: [0, 1, 0] }}
|
||||||
|
transition={{ duration: 0.8, repeat: Infinity }}
|
||||||
|
className="absolute -left-1 top-0 h-full w-[3px] bg-canina-blue"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Hero({ onProfileClick, onShopNavigate }: { onProfileClick?: () => void, onShopNavigate?: () => void }) {
|
||||||
|
return (
|
||||||
|
<section className="relative overflow-hidden bg-white py-20 lg:py-32 border-b border-medical-gray-100">
|
||||||
|
{/* Background patterns */}
|
||||||
|
<div className="absolute top-0 left-0 w-full h-full opacity-[0.03] pointer-events-none select-none overflow-hidden">
|
||||||
|
<div className="absolute top-0 right-0 w-[800px] h-[800px] bg-canina-blue rounded-full blur-[120px] -mr-96 -mt-96" />
|
||||||
|
<div className="absolute bottom-0 left-0 w-[600px] h-[600px] bg-medical-gray-500 rounded-full blur-[100px] -ml-40 -mb-40" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 relative flex flex-col lg:flex-row items-center gap-12">
|
||||||
|
{/* Content */}
|
||||||
|
<div className="lg:w-1/2 text-center lg:text-right">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
>
|
||||||
|
<div className="inline-flex items-center gap-2 px-3 py-1 bg-canina-blue/5 border border-canina-blue/10 rounded-full mb-6">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-canina-blue animate-pulse" />
|
||||||
|
<span className="text-canina-blue text-xs font-bold uppercase tracking-widest leading-none">تخصص دارویی از آلمان</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-4xl lg:text-7xl font-black text-medical-gray-900 leading-[1.1] mb-6">
|
||||||
|
تخصص آلمانی در خدمت <br />
|
||||||
|
<span className="text-canina-blue font-vazir">سلامت پتهای خانگی</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-lg lg:text-xl text-medical-gray-600 mb-10 max-w-2xl mx-auto lg:mx-0 leading-relaxed font-vazir">
|
||||||
|
بیش از <span className="font-bold text-canina-blue">۴۰ سال</span> تجربه نوآورانه در تولید مکملهای درمانی با بالاترین استاندارد کیفی «گرید دارویی اختصاصی». راهکار هوشمند برای هر نیاز بالینی.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap justify-center lg:justify-start gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const element = document.getElementById('canina-advisor');
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="bg-medical-gray-900 text-white px-10 py-5 rounded-full font-bold text-lg hover:bg-canina-blue hover:shadow-2xl transition-all flex items-center gap-2 group font-vazir"
|
||||||
|
>
|
||||||
|
دستیار سلامت پت
|
||||||
|
<ChevronLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onShopNavigate}
|
||||||
|
className="bg-white border-2 border-medical-gray-200 text-medical-gray-700 px-10 py-5 rounded-full font-bold text-lg hover:border-canina-blue hover:text-canina-blue transition-all font-vazir"
|
||||||
|
>
|
||||||
|
مشاهده محصولات
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.8, duration: 0.6 }}
|
||||||
|
className="grid grid-cols-3 gap-8 mt-16 pt-8 border-t border-medical-gray-100"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center lg:items-end">
|
||||||
|
<Typewriter text="۱۹۸۴ سال تأسیس" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center lg:items-end">
|
||||||
|
<StatCounter target={40} />
|
||||||
|
<div className="text-xs text-medical-gray-500 font-bold uppercase tracking-tight mt-3 font-vazir">نمایندگی فعال</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center lg:items-end">
|
||||||
|
<div className="relative overflow-hidden group">
|
||||||
|
<motion.div
|
||||||
|
className="text-4xl lg:text-6xl font-black text-canina-blue font-vazir relative z-10"
|
||||||
|
>
|
||||||
|
۱۰۰٪
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
initial={{ x: "-100%" }}
|
||||||
|
animate={{ x: "200%" }}
|
||||||
|
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
|
||||||
|
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/40 to-transparent skew-x-12 z-20 pointer-events-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-medical-gray-500 font-bold uppercase tracking-tight mt-3 font-vazir">فرمول آلمانی</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Visual Element */}
|
||||||
|
<div className="lg:w-1/2 relative">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9, rotate: -5 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, rotate: 0 }}
|
||||||
|
transition={{ duration: 1, ease: "easeOut" }}
|
||||||
|
className="relative z-10"
|
||||||
|
>
|
||||||
|
<div className="aspect-square bg-medical-gray-100 rounded-[2rem] overflow-hidden border-4 border-white shadow-2xl relative">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1583337130417-3346a1be7dee?auto=format&fit=crop&q=80&w=800"
|
||||||
|
alt="German Veterinary Expert"
|
||||||
|
className="w-full h-full object-cover grayscale-[20%] hover:grayscale-0 transition-all duration-700"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-8 bg-gradient-to-t from-black/80 to-transparent text-white text-right">
|
||||||
|
<div className="text-sm font-medium mb-1 opacity-80 uppercase tracking-widest text-[10px]">سرآمد علمی در پزشکی پتها</div>
|
||||||
|
<div className="text-xl font-bold italic tracking-tighter">مکملهای تایید شده دامپزشکی</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Badge */}
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
|
||||||
|
className="absolute -top-6 -right-6 lg:-top-10 lg:-right-10 z-20 w-32 h-32 lg:w-40 lg:h-40 bg-white shadow-xl rounded-full p-1 border-2 border-dashed border-canina-blue flex items-center justify-center text-center p-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<span className="text-2xl lg:text-3xl font-black text-canina-blue leading-none tracking-tighter">DE</span>
|
||||||
|
<span className="text-[8px] lg:text-[10px] font-bold text-medical-gray-500 uppercase tracking-widest mt-1">استاندارد کیفی آلمان</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
155
src/components/IngredientWiki.tsx
Normal file
155
src/components/IngredientWiki.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { INGREDIENTS_WIKI, PRODUCTS, Product } from "../data/products";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { FlaskConical, CheckCircle2, ChevronRight, ChevronLeft, Beaker } from "lucide-react";
|
||||||
|
|
||||||
|
export default function IngredientWiki({ onProductClick, onBack }: { onProductClick: (p: Product) => void, onBack?: () => void }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white py-12 px-4 overflow-hidden font-vazir" dir="rtl">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
|
||||||
|
{/* Mobile Navigation & Breadcrumbs */}
|
||||||
|
<div className="lg:hidden mb-12 space-y-4">
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="flex items-center gap-2 text-medical-gray-500 font-bold hover:text-canina-blue transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
<span>بازگشت</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest overflow-x-auto whitespace-nowrap pb-2">
|
||||||
|
<span className="cursor-pointer hover:text-canina-blue" onClick={onBack}>کانینا</span>
|
||||||
|
<ChevronLeft className="w-2.5 h-2.5" />
|
||||||
|
<span className="text-canina-blue">دانشنامه علمی</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Breadcrumbs */}
|
||||||
|
<div className="hidden lg:flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-12">
|
||||||
|
<span className="cursor-pointer hover:text-canina-blue" onClick={onBack}>خانه</span>
|
||||||
|
<ChevronLeft className="w-2.5 h-2.5" />
|
||||||
|
<span className="text-canina-blue">دانشنامه مواد نایاب و ارگانیک</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center text-center mb-20">
|
||||||
|
<div className="inline-flex items-center gap-2 px-4 py-2 bg-canina-blue/5 border border-canina-blue/10 rounded-full text-canina-blue text-xs font-black uppercase tracking-widest mb-6">
|
||||||
|
<FlaskConical className="w-4 h-4" />
|
||||||
|
علم در خدمت کیفیت
|
||||||
|
</div>
|
||||||
|
<h2 className="text-4xl lg:text-6xl font-black text-medical-gray-900 leading-tight md:max-w-4xl">
|
||||||
|
دانشنامه مواد <span className="text-canina-blue italic">نایاب و ارگانیک</span> کانینا
|
||||||
|
</h2>
|
||||||
|
<p className="mt-6 text-lg text-medical-gray-500 max-w-2xl leading-relaxed">
|
||||||
|
ما از مواد اولیهای استفاده میکنیم که در سطح دارویی فرآوری شدهاند. در اینجا با قدرت بیولوژیک این ترکیبات آشنا شوید.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-32">
|
||||||
|
{INGREDIENTS_WIKI.map((ing, idx) => {
|
||||||
|
const hasIngredient = PRODUCTS.filter(p =>
|
||||||
|
p.main_ingredients.some(mi => mi.toLowerCase().includes(ing.name.toLowerCase())) ||
|
||||||
|
p.main_ingredients.some(mi => mi.toLowerCase().includes(ing.id.replace('-', ' ')))
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={ing.id} className={`flex flex-col lg:flex-row items-center gap-16 ${idx % 2 === 1 ? 'lg:flex-row-reverse' : ''}`}>
|
||||||
|
{/* Content */}
|
||||||
|
<div className="lg:w-1/2 space-y-8">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: idx % 2 === 1 ? 40 : -40 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.8 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
<div className="w-14 h-14 rounded-3xl bg-medical-gray-900 flex items-center justify-center text-white shadow-xl">
|
||||||
|
<Beaker className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl font-black text-medical-gray-900">{ing.name}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-lg text-medical-gray-600 leading-relaxed mb-8">
|
||||||
|
{ing.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 mb-10">
|
||||||
|
{ing.benefits.map((benefit, bIdx) => (
|
||||||
|
<div key={bIdx} className="flex items-start gap-4 p-4 bg-medical-gray-50 rounded-2xl border border-medical-gray-100">
|
||||||
|
<CheckCircle2 className="w-6 h-6 text-canina-blue flex-shrink-0" />
|
||||||
|
<span className="text-sm font-bold text-medical-gray-700">{benefit}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Products Linking */}
|
||||||
|
<div className="lg:w-1/2 relative">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="bg-medical-gray-100 rounded-[4rem] p-12 relative overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 right-0 p-8">
|
||||||
|
<div className="text-[10px] font-black text-medical-gray-400 uppercase tracking-[0.3em]">موجود در محصولات</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 relative z-10">
|
||||||
|
{hasIngredient.length > 0 ? hasIngredient.map(p => (
|
||||||
|
<div
|
||||||
|
key={p.id}
|
||||||
|
onClick={() => onProductClick(p)}
|
||||||
|
className="bg-white rounded-[2.5rem] p-6 shadow-xl hover:scale-105 transition-transform cursor-pointer group"
|
||||||
|
>
|
||||||
|
<img src={p.image} alt={p.name} className="w-24 h-24 object-contain mx-auto mb-4" referrerPolicy="no-referrer" />
|
||||||
|
<h4 className="text-sm font-black text-center text-medical-gray-900 group-hover:text-canina-blue transition-colors px-2">{p.name}</h4>
|
||||||
|
</div>
|
||||||
|
)) : (
|
||||||
|
<div className="col-span-full py-12 text-center text-medical-gray-400 font-bold italic">
|
||||||
|
اطلاعات محصولات مرتبط به زودی اضافه خواهد شد.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Decorative backdrop */}
|
||||||
|
<div className="absolute -bottom-20 -left-20 w-80 h-80 bg-canina-blue rounded-full blur-[100px] opacity-10" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quality Badge */}
|
||||||
|
<div className="mt-32 p-12 bg-medical-gray-900 rounded-[4rem] text-center text-white relative overflow-hidden">
|
||||||
|
<div className="absolute inset-0 opacity-10 mix-blend-overlay">
|
||||||
|
<div className="grid grid-cols-12 h-full w-full">
|
||||||
|
{Array.from({length: 120}).map((_, i) => (
|
||||||
|
<div key={i} className="border border-white/20" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative z-10 max-w-3xl mx-auto">
|
||||||
|
<h3 className="text-3xl lg:text-5xl font-black mb-8 leading-tight italic">تمام مواد اولیه ما دارای گواهینامه <br /> <span className="text-canina-blue">گرید دارویی اختصاصی</span> هستند.</h3>
|
||||||
|
<p className="text-white/60 text-lg mb-10">این یعنی استانداردی فراتر از مکملهای معمولی، مشابه استانداردهای تولید دارو در اتحادیه اروپا.</p>
|
||||||
|
<div className="flex justify-center gap-12">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="text-2xl font-black font-vazir text-canina-blue">۱۰۰٪</div>
|
||||||
|
<div className="text-[10px] font-bold uppercase tracking-widest text-white/40 mt-1">تست آزمایشگاهی</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="text-2xl font-black font-vazir">GMP</div>
|
||||||
|
<div className="text-[10px] font-bold uppercase tracking-widest text-white/40 mt-1">استاندارد تولید</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="text-2xl font-black font-vazir">ISO</div>
|
||||||
|
<div className="text-[10px] font-bold uppercase tracking-widest text-white/40 mt-1">کنترل کیفیت</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
106
src/components/LoginModal.tsx
Normal file
106
src/components/LoginModal.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { X, ShieldCheck, Mail, Lock, LogIn, UserPlus } from "lucide-react";
|
||||||
|
|
||||||
|
interface LoginModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onLogin: () => void;
|
||||||
|
petName?: string;
|
||||||
|
isAdvisorContext?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoginModal({ isOpen, onClose, onLogin, petName, isAdvisorContext }: LoginModalProps) {
|
||||||
|
const [view, setView] = useState<"login" | "register">("login");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute inset-0 bg-medical-gray-900/60 backdrop-blur-md"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
className="bg-white w-full max-w-md rounded-[3.5rem] p-10 relative z-10 shadow-2xl overflow-hidden font-vazir"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 left-0 w-full h-2 bg-canina-blue" />
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-8 left-8 text-medical-gray-400 hover:text-medical-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center mb-10">
|
||||||
|
<div className="w-20 h-20 bg-canina-blue/10 rounded-[2rem] flex items-center justify-center mx-auto mb-6">
|
||||||
|
<ShieldCheck className="w-10 h-10 text-canina-blue" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl font-black text-medical-gray-900 mb-2 italic">
|
||||||
|
{view === "login" ? "ورود به کانینا" : "عضویت در کانینا"}
|
||||||
|
</h3>
|
||||||
|
{isAdvisorContext ? (
|
||||||
|
<p className="text-medical-gray-500 font-bold leading-relaxed px-4">
|
||||||
|
تحلیل سلامت <span className="text-canina-blue">{petName || "همدم شما"}</span> آماده است! برای مشاهده رژیم مکمل پیشنهادی و ذخیره دائمی شناسنامه، وارد شوید.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-medical-gray-500 font-bold">برای دسترسی به پنل مدیریت سلامت، وارد شوید</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 mb-8">
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-medical-gray-400" />
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="ایمیل یا شماره موبایل"
|
||||||
|
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 pr-12 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none font-bold"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-medical-gray-400" />
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="رمز عبور"
|
||||||
|
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 pr-12 pl-4 focus:ring-2 focus:ring-canina-blue/20 outline-none font-bold"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{view === "login" && (
|
||||||
|
<div className="text-left">
|
||||||
|
<button className="text-xs font-black text-canina-blue hover:underline italic">رمز عبور را فراموش کردهاید؟</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onLogin}
|
||||||
|
className="w-full py-5 bg-canina-blue text-white rounded-2xl font-black text-xl hover:bg-indigo-700 transition-all shadow-xl shadow-canina-blue/20 flex items-center justify-center gap-3 mb-6"
|
||||||
|
>
|
||||||
|
{view === "login" ? <LogIn className="w-6 h-6" /> : <UserPlus className="w-6 h-6" />}
|
||||||
|
{view === "login" ? "ورود به حساب" : "ایجاد حساب کاربری"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-medical-gray-400 font-bold">
|
||||||
|
{view === "login" ? "هنوز حساب ندارید؟" : "قبلاً ثبتنام کردهاید؟"}{' '}
|
||||||
|
<button
|
||||||
|
onClick={() => setView(view === "login" ? "register" : "login")}
|
||||||
|
className="text-canina-blue font-black hover:underline"
|
||||||
|
>
|
||||||
|
{view === "login" ? "عضویت رایگان" : "ورود"}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
67
src/components/NetworkBanner.tsx
Normal file
67
src/components/NetworkBanner.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNetworkStatus } from '../hooks/useNetworkStatus';
|
||||||
|
import { WifiOff, Wifi, X } from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
|
||||||
|
export const NetworkBanner = () => {
|
||||||
|
const isOnline = useNetworkStatus();
|
||||||
|
const [showStatus, setShowStatus] = useState(false);
|
||||||
|
const [wasOffline, setWasOffline] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOnline) {
|
||||||
|
setShowStatus(true);
|
||||||
|
setWasOffline(true);
|
||||||
|
} else if (wasOffline) {
|
||||||
|
// Show "Back Online" message briefly
|
||||||
|
setShowStatus(true);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setShowStatus(false);
|
||||||
|
setWasOffline(false);
|
||||||
|
}, 3000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isOnline, wasOffline]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{showStatus && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: -100 }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
exit={{ y: -100 }}
|
||||||
|
className={`fixed top-0 left-0 right-0 z-[100] text-white py-3 px-4 flex items-center justify-between shadow-2xl backdrop-blur-md ${
|
||||||
|
isOnline ? 'bg-emerald-500/90' : 'bg-rose-500/90'
|
||||||
|
}`}
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{isOnline ? (
|
||||||
|
<div className="bg-white/20 p-2 rounded-xl">
|
||||||
|
<Wifi className="w-5 h-5 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white/20 p-2 rounded-xl">
|
||||||
|
<WifiOff className="w-5 h-5 animate-bounce" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className="font-black text-sm">
|
||||||
|
{isOnline
|
||||||
|
? 'اتصال اینترنت مجدداً برقرار شد.'
|
||||||
|
: 'اتصال اینترنت شما قطع شده است. در حال تلاش برای اتصال مجدد...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowStatus(false)}
|
||||||
|
className="hover:bg-white/10 p-2 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
||||||
264
src/components/OrderDetailsModal.tsx
Normal file
264
src/components/OrderDetailsModal.tsx
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import {
|
||||||
|
X,
|
||||||
|
Printer,
|
||||||
|
Download,
|
||||||
|
Package,
|
||||||
|
ShoppingBag,
|
||||||
|
MapPin,
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
CreditCard,
|
||||||
|
CheckCircle2,
|
||||||
|
AlertCircle,
|
||||||
|
Truck
|
||||||
|
} from "lucide-react";
|
||||||
|
import { toPersian, cn } from "../lib/utils";
|
||||||
|
import SafeImage from "./SafeImage";
|
||||||
|
|
||||||
|
interface OrderDetailsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
order: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OrderDetailsModal({ isOpen, onClose, order }: OrderDetailsModalProps) {
|
||||||
|
if (!order) return null;
|
||||||
|
|
||||||
|
const handlePrint = () => {
|
||||||
|
// Basic print logic: open a new window with a simplified version of the invoice
|
||||||
|
const printWindow = window.open('', '_blank');
|
||||||
|
if (!printWindow) return;
|
||||||
|
|
||||||
|
const itemsHtml = (order.items || []).map((item: any) => `
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #eee;">${item.product.name}</td>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: center;">${toPersian(item.quantity)}</td>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: left;">${toPersian(item.product.priceValue.toLocaleString())} تومان</td>
|
||||||
|
<td style="padding: 12px; border-bottom: 1px solid #eee; text-align: left;">${toPersian((item.product.priceValue * item.quantity).toLocaleString())} تومان</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
printWindow.document.write(`
|
||||||
|
<html dir="rtl">
|
||||||
|
<head>
|
||||||
|
<title>فاکتور ${order.id}</title>
|
||||||
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Vazirmatn';
|
||||||
|
src: url('https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/fonts/webfonts/Vazirmatn-Regular.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
body { font-family: 'Vazirmatn', sans-serif; padding: 40px; color: #333; }
|
||||||
|
.header { display: flex; justify-between: space-between; align-items: center; border-bottom: 2px solid #0055FF; pb: 20px; mb: 40px; }
|
||||||
|
.logo { font-size: 24px; font-weight: 900; color: #0055FF; }
|
||||||
|
.info { margin-bottom: 40px; display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
th { background: #f8f9fa; text-align: right; padding: 12px; }
|
||||||
|
.total-section { margin-top: 40px; text-align: left; }
|
||||||
|
.footer { margin-top: 60px; font-size: 12px; color: #888; text-align: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">CANINA | کانینا</div>
|
||||||
|
<div>تاریخ: ${toPersian(new Date(order.date).toLocaleDateString('fa-IR'))}</div>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<div>
|
||||||
|
<strong>شماره فاکتور:</strong> ${toPersian(order.id)}<br/>
|
||||||
|
<strong>کد پیگیری:</strong> ${toPersian(order.trackingNumber)}
|
||||||
|
</div>
|
||||||
|
<div style="text-align: left;">
|
||||||
|
<strong>وضعیت:</strong> ${order.status === 'delivered' ? 'تسویه شده' : 'در حال پردازش'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>محصول</th>
|
||||||
|
<th>تعداد</th>
|
||||||
|
<th>قیمت واحد</th>
|
||||||
|
<th>جمع کل</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${itemsHtml}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="total-section">
|
||||||
|
<p>جمع کل فاکتور: ${toPersian(order.total.toLocaleString())} تومان</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
این فاکتور به صورت سیستمی صادر شده و فاقد مهر و امضای فیزیکی است.
|
||||||
|
</div>
|
||||||
|
<script>window.print();</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
printWindow.document.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'delivered': return <CheckCircle2 className="w-5 h-5 text-green-500" />;
|
||||||
|
case 'shipped': return <Truck className="w-5 h-5 text-blue-500" />;
|
||||||
|
default: return <Clock className="w-5 h-5 text-orange-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusLabel = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'delivered': return 'تحویل شده';
|
||||||
|
case 'shipped': return 'ارسال شده';
|
||||||
|
default: return 'در حال پردازش';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderDate = new Date(order.date);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute inset-0 bg-medical-gray-900/60 backdrop-blur-md"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
className="bg-white w-full max-w-2xl rounded-[3.5rem] relative z-10 shadow-2xl overflow-hidden font-vazir text-right"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-medical-gray-50 px-10 py-8 flex items-center justify-between border-b border-medical-gray-100">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-14 h-14 bg-canina-blue rounded-2xl flex items-center justify-center text-white shadow-lg">
|
||||||
|
<Package className="w-7 h-7" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-black text-medical-gray-900 italic">جزئیات سفارش {toPersian(order.id)}</h3>
|
||||||
|
<p className="text-xs font-bold text-medical-gray-400">ثبت شده در {toPersian(orderDate.toLocaleDateString("fa-IR"))}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-white border border-medical-gray-200 text-medical-gray-400 hover:text-red-500 transition-all"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Scrollable */}
|
||||||
|
<div className="max-h-[70vh] overflow-y-auto px-10 py-8 space-y-10 custom-scrollbar">
|
||||||
|
{/* Summary Stats */}
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="bg-medical-gray-50 p-4 rounded-2xl">
|
||||||
|
<div className="text-[9px] font-black text-medical-gray-400 uppercase tracking-widest mb-1">وضعیت تحویل</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getStatusIcon(order.status)}
|
||||||
|
<span className="text-sm font-black text-medical-gray-900">{getStatusLabel(order.status)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-medical-gray-50 p-4 rounded-2xl">
|
||||||
|
<div className="text-[9px] font-black text-medical-gray-400 uppercase tracking-widest mb-1">کد رهگیری</div>
|
||||||
|
<div className="text-sm font-mono font-bold text-canina-blue">{toPersian(order.trackingNumber)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-medical-gray-50 p-4 rounded-2xl">
|
||||||
|
<div className="text-[9px] font-black text-medical-gray-400 uppercase tracking-widest mb-1">زمان ثبت</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="w-4 h-4 text-medical-gray-400" />
|
||||||
|
<span className="text-sm font-black text-medical-gray-900">{toPersian(orderDate.toLocaleTimeString("fa-IR", { hour: '2-digit', minute: '2-digit' }))}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Items List */}
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-black text-medical-gray-900 mb-6 flex items-center gap-2">
|
||||||
|
<ShoppingBag className="w-4 h-4 text-canina-blue" />
|
||||||
|
سبد محصولات خریده شده
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{(order.items || []).map((item: any) => (
|
||||||
|
<div key={item.product.id} className="flex items-center justify-between p-4 border border-medical-gray-100 rounded-2xl hover:border-canina-blue transition-all">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-16 h-16 bg-medical-gray-50 rounded-xl p-2 flex items-center justify-center shrink-0">
|
||||||
|
<SafeImage
|
||||||
|
src={item.product.image}
|
||||||
|
alt={item.product.name}
|
||||||
|
className="w-full h-full"
|
||||||
|
imgClassName="object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="font-black text-medical-gray-900 leading-tight mb-1">{item.product.name}</h5>
|
||||||
|
<div className="text-[10px] font-bold text-medical-gray-400">{toPersian(item.quantity)} عدد × {toPersian(item.product.priceValue.toLocaleString())} تومان</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-lg font-black text-canina-blue italic">{toPersian((item.product.priceValue * item.quantity).toLocaleString())} <span className="text-[10px] not-italic">تومان</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Shipping & Payment */}
|
||||||
|
<div className="grid md:grid-cols-2 gap-8 pt-6 border-t border-medical-gray-100">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-black text-medical-gray-900 mb-4 flex items-center gap-2">
|
||||||
|
<MapPin className="w-4 h-4 text-canina-blue" />
|
||||||
|
اطلاعات ارسال
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs font-bold text-medical-gray-500 leading-relaxed">
|
||||||
|
تهران، خیابان ولیعصر، بالاتر از میدان ونک، بنبست آفتاب، پلاک ۱۲، واحد ۳
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-black text-medical-gray-900 mb-4 flex items-center gap-2">
|
||||||
|
<CreditCard className="w-4 h-4 text-canina-blue" />
|
||||||
|
جزئیات پرداخت
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-xs font-bold text-medical-gray-400">
|
||||||
|
<span>جمع ناخالص</span>
|
||||||
|
<span>{toPersian(order.total.toLocaleString())} تومان</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs font-bold text-red-500">
|
||||||
|
<span>تخفیف هوشمند</span>
|
||||||
|
<span>{toPersian("۰")} تومان</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-base font-black text-medical-gray-900 pt-2 border-t border-medical-gray-50 italic">
|
||||||
|
<span>مبلغ نهایی</span>
|
||||||
|
<span>{toPersian(order.total.toLocaleString())} تومان</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Actions */}
|
||||||
|
<div className="p-10 bg-medical-gray-50 border-t border-medical-gray-100 flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={handlePrint}
|
||||||
|
className="flex-1 py-5 bg-white border border-medical-gray-200 text-medical-gray-900 rounded-2xl font-black text-sm flex items-center justify-center gap-3 hover:bg-medical-gray-100 transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
<Download className="w-5 h-5" />
|
||||||
|
دانلود فاکتور رسمی
|
||||||
|
</button>
|
||||||
|
<button className="flex items-center justify-center w-14 h-14 bg-medical-gray-900 text-white rounded-2xl hover:bg-canina-blue transition-all shadow-lg shadow-black/10">
|
||||||
|
<Printer className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
109
src/components/OrderSuccess.tsx
Normal file
109
src/components/OrderSuccess.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import {
|
||||||
|
CheckCircle2,
|
||||||
|
Package,
|
||||||
|
Calendar,
|
||||||
|
MapPin,
|
||||||
|
ChevronLeft,
|
||||||
|
Sparkles,
|
||||||
|
Heart
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useCartStore } from "../store/cartStore";
|
||||||
|
import { usePetStore } from "../store/usePetStore";
|
||||||
|
|
||||||
|
export default function OrderSuccess({ orderId, onNavigate }: { orderId: string; onNavigate: (v: any) => void }) {
|
||||||
|
const { orders } = useCartStore();
|
||||||
|
const { getActivePet } = usePetStore();
|
||||||
|
const order = orders.find(o => o.id === orderId);
|
||||||
|
const activePet = getActivePet();
|
||||||
|
|
||||||
|
if (!order) return null;
|
||||||
|
|
||||||
|
const hasCanhydrox = order.items.some(i => i.product.id === 'canhydrox-gag');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-medical-gray-50 py-20 px-6 font-vazir" dir="rtl">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
className="bg-white rounded-[4rem] border border-medical-gray-200 p-12 text-center shadow-2xl relative overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Confetti-like background elements */}
|
||||||
|
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-canina-blue via-green-500 to-canina-blue" />
|
||||||
|
|
||||||
|
<div className="w-24 h-24 bg-green-50 text-green-600 rounded-full flex items-center justify-center mx-auto mb-10 shadow-inner">
|
||||||
|
<CheckCircle2 className="w-12 h-12" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-4xl font-black text-medical-gray-900 italic mb-4">سفارش شما با موفقیت ثبت شد!</h2>
|
||||||
|
<p className="text-medical-gray-500 font-bold mb-10">شماره سفارش: <span className="text-canina-blue font-mono">{order.id}</span></p>
|
||||||
|
|
||||||
|
{/* Empathetic Message */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="p-8 bg-canina-blue/5 border border-canina-blue/10 rounded-[3rem] mb-12 text-right relative"
|
||||||
|
>
|
||||||
|
<Sparkles className="absolute top-6 left-6 w-8 h-8 text-canina-blue opacity-20" />
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<div className="w-10 h-10 bg-white rounded-2xl flex items-center justify-center text-canina-blue shadow-sm">
|
||||||
|
<Heart className="w-6 h-6 fill-current" />
|
||||||
|
</div>
|
||||||
|
<h4 className="text-lg font-black text-canina-blue italic">خیالت راحت باشه!</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-bold text-canina-blue/80 leading-relaxed">
|
||||||
|
{activePet ? (
|
||||||
|
<>
|
||||||
|
انتخاب درستی برای <span className="text-canina-blue font-black">{activePet.name}</span> کردی.
|
||||||
|
{hasCanhydrox ? (
|
||||||
|
` مکمل Canhydrox با فرمول آلمانیاش دقیقاً همون چیزیه که استخوانهای ${activePet.name} برای بازیگوشی دوباره بهش نیاز دارن.`
|
||||||
|
) : (
|
||||||
|
` محصولات انتخابی شما با بالاترین استانداردهای دارویی آلمان تولید شدهاند و به زودی به دست ${activePet.name} میرسند.`
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"سفارش شما با رعایت تمامی استانداردهای کیفی آلمان در حال دستهبندی است. از اعتماد شما به سلامت پتها سپاسگزاریم."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6 text-right mb-12">
|
||||||
|
<div className="p-6 bg-medical-gray-50 rounded-3xl border border-medical-gray-100 flex items-center gap-4">
|
||||||
|
<Package className="w-8 h-8 text-medical-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">وضعیت ارسال</p>
|
||||||
|
<p className="text-sm font-black text-medical-gray-900 italic">در حال پردازش در انبار</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 bg-medical-gray-50 rounded-3xl border border-medical-gray-100 flex items-center gap-4">
|
||||||
|
<Calendar className="w-8 h-8 text-medical-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest">تخمین تحویل</p>
|
||||||
|
<p className="text-sm font-black text-medical-gray-900 italic">۴۸ تا ۷۲ ساعت کاری</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<button
|
||||||
|
onClick={() => onNavigate('order-tracking')}
|
||||||
|
className="w-full bg-medical-gray-900 text-white py-5 rounded-2xl font-black text-lg hover:bg-canina-blue transition-all"
|
||||||
|
>
|
||||||
|
مشاهده و پیگیری زنده سفارش
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onNavigate('shop')}
|
||||||
|
className="flex items-center justify-center gap-2 w-full text-medical-gray-400 font-bold hover:text-canina-blue transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
بازگشت به فروشگاه
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
167
src/components/OrderTracking.tsx
Normal file
167
src/components/OrderTracking.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import {
|
||||||
|
Package,
|
||||||
|
Truck,
|
||||||
|
CheckCircle2,
|
||||||
|
MapPin,
|
||||||
|
Search,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronLeft,
|
||||||
|
Clock,
|
||||||
|
ExternalLink
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useCartStore } from "../store/cartStore";
|
||||||
|
|
||||||
|
export default function OrderTracking({ onNavigate, onBack }: { onNavigate: (v: any) => void, onBack?: () => void }) {
|
||||||
|
const { orders } = useCartStore();
|
||||||
|
const [searchId, setSearchId] = useState("");
|
||||||
|
const [activeOrder, setActiveOrder] = useState(orders[0] || null);
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
const found = orders.find(o => o.id === searchId || o.trackingNumber === searchId);
|
||||||
|
if (found) {
|
||||||
|
setActiveOrder(found);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ label: 'ثبت سفارش', sub: 'تایید شده', status: 'completed', icon: <Clock className="w-5 h-5" /> },
|
||||||
|
{ label: 'آمادهسازی در انبار', sub: 'در حال بستهبندی', status: 'active', icon: <Package className="w-5 h-5" /> },
|
||||||
|
{ label: 'تحویل به پست/کوریر', sub: 'ارسال شده', status: 'pending', icon: <Truck className="w-5 h-5" /> },
|
||||||
|
{ label: 'تحویل نهایی', sub: 'در مقصد', status: 'pending', icon: <CheckCircle2 className="w-5 h-5" /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-medical-gray-50 py-12 px-6 font-vazir" dir="rtl">
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
|
||||||
|
{/* Mobile Navigation & Breadcrumbs */}
|
||||||
|
<div className="lg:hidden mb-12 space-y-4">
|
||||||
|
<button
|
||||||
|
onClick={onBack || (() => onNavigate('home'))}
|
||||||
|
className="flex items-center gap-2 text-medical-gray-500 font-bold hover:text-canina-blue transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5 font-vazir" />
|
||||||
|
<span>بازگشت</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest overflow-x-auto whitespace-nowrap pb-2">
|
||||||
|
<span className="cursor-pointer hover:text-canina-blue" onClick={onBack || (() => onNavigate('home'))}>کانینا</span>
|
||||||
|
<ChevronLeft className="w-2.5 h-2.5" />
|
||||||
|
<span className="text-canina-blue">پیگیری سفارش</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Breadcrumbs */}
|
||||||
|
<div className="hidden lg:flex items-center gap-2 text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-12">
|
||||||
|
<span className="cursor-pointer hover:text-canina-blue" onClick={onBack || (() => onNavigate('home'))}>خانه</span>
|
||||||
|
<ChevronLeft className="w-2.5 h-2.5" />
|
||||||
|
<span className="text-canina-blue">رهگیری هوشمند محموله</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between mb-12">
|
||||||
|
<h2 className="text-3xl font-black text-medical-gray-900 italic">وضعیت لحظهای سفارش</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="bg-white rounded-[2.5rem] p-8 border border-medical-gray-200 shadow-sm mb-10">
|
||||||
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<Search className="absolute right-4 top-1/2 -translate-y-1/2 w-5 h-5 text-medical-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="شماره سفارش (CN-XXXXX) یا کد رهگیری پستی را وارد کنید..."
|
||||||
|
value={searchId}
|
||||||
|
onChange={(e) => setSearchId(e.target.value)}
|
||||||
|
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 pr-12 pl-6 text-sm focus:ring-2 focus:ring-canina-blue/20 outline-none font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSearch}
|
||||||
|
className="bg-medical-gray-900 text-white px-10 py-4 rounded-2xl font-black italic hover:bg-canina-blue transition-all"
|
||||||
|
>
|
||||||
|
رهگیری آنی
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeOrder ? (
|
||||||
|
<div className="grid lg:grid-cols-12 gap-10">
|
||||||
|
{/* Status Timeline */}
|
||||||
|
<div className="lg:col-span-8 bg-white rounded-[3rem] p-10 border border-medical-gray-200 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between mb-12">
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-1">کد رهگیری پستی</p>
|
||||||
|
<p className="text-sm font-mono font-bold text-canina-blue">{activeOrder.trackingNumber}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<p className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-1">تاریخ ثبت</p>
|
||||||
|
<p className="text-sm font-bold text-medical-gray-900">{new Date(activeOrder.date).toLocaleDateString('fa-IR')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative space-y-12 pr-6">
|
||||||
|
<div className="absolute top-0 right-[23px] bottom-0 w-0.5 bg-medical-gray-100" />
|
||||||
|
|
||||||
|
{steps.map((step, i) => (
|
||||||
|
<div key={i} className={`relative flex items-center gap-8 ${step.status === 'pending' ? 'opacity-30' : ''}`}>
|
||||||
|
<div className={`w-12 h-12 rounded-2xl flex items-center justify-center z-10 shadow-sm transition-all ${step.status === 'completed' ? 'bg-green-600 text-white' : step.status === 'active' ? 'bg-canina-blue text-white ring-8 ring-canina-blue/10 scale-110' : 'bg-medical-gray-50 text-medical-gray-300 border border-medical-gray-100'}`}>
|
||||||
|
{step.status === 'completed' ? <CheckCircle2 className="w-6 h-6" /> : step.icon}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-black text-medical-gray-900">{step.label}</h4>
|
||||||
|
<p className="text-xs font-bold text-medical-gray-500">{step.sub}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sidebar Details */}
|
||||||
|
<div className="lg:col-span-4 space-y-6">
|
||||||
|
<div className="bg-medical-gray-900 rounded-[2.5rem] p-8 text-white">
|
||||||
|
<h3 className="text-lg font-black italic mb-6 border-b border-white/10 pb-4">محتویات محموله</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{activeOrder.items.map(item => (
|
||||||
|
<div key={item.product.id} className="flex justify-between items-center text-xs font-bold">
|
||||||
|
<span className="text-white/60">{item.product.name}</span>
|
||||||
|
<span>{item.quantity} عدد</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 pt-6 border-t border-white/10 flex justify-between items-center font-black">
|
||||||
|
<span className="text-xs italic">مجموع فاکتور</span>
|
||||||
|
<span className="text-lg text-canina-blue">{activeOrder.total.toLocaleString()} تومان</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-[2rem] p-6 border border-medical-gray-200">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="w-10 h-10 bg-canina-blue/10 text-canina-blue rounded-xl flex items-center justify-center">
|
||||||
|
<MapPin className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h4 className="text-xs font-black italic">آدرس تحویل</h4>
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] font-bold text-medical-gray-500 leading-relaxed mb-4">
|
||||||
|
تهران، خیابان ولیعصر، بالاتر از میدان ونک، بنبست آفتاب، پلاک ۱۲، واحد ۳
|
||||||
|
</p>
|
||||||
|
<button className="w-full py-3 bg-medical-gray-50 text-medical-gray-900 rounded-xl text-[10px] font-black flex items-center justify-center gap-2 hover:bg-medical-gray-100 transition-all">
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
مشاهده در نقشه
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-20 bg-white rounded-[3rem] border border-dashed border-medical-gray-200">
|
||||||
|
<div className="w-20 h-20 bg-medical-gray-50 rounded-full flex items-center justify-center mx-auto mb-6 text-medical-gray-300">
|
||||||
|
<Package className="w-10 h-10" />
|
||||||
|
</div>
|
||||||
|
<p className="text-medical-gray-500 font-bold">سفارشی یافت نشد. لطفاً شماره سفارش صحیح را وارد کنید.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
1184
src/components/PetProfile.tsx
Normal file
1184
src/components/PetProfile.tsx
Normal file
File diff suppressed because it is too large
Load Diff
582
src/components/ProductPage.tsx
Normal file
582
src/components/ProductPage.tsx
Normal file
@ -0,0 +1,582 @@
|
|||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Calculator as CalcIcon,
|
||||||
|
ChevronLeft,
|
||||||
|
ShieldCheck,
|
||||||
|
CheckCircle2,
|
||||||
|
Clock,
|
||||||
|
FlaskConical,
|
||||||
|
Stethoscope,
|
||||||
|
Info,
|
||||||
|
CalendarDays,
|
||||||
|
Dog,
|
||||||
|
Gamepad2,
|
||||||
|
X,
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
Bell,
|
||||||
|
Sparkles,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronDown,
|
||||||
|
Activity,
|
||||||
|
Heart,
|
||||||
|
Zap,
|
||||||
|
Flame,
|
||||||
|
Star,
|
||||||
|
Users,
|
||||||
|
ShoppingBag
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
const ICON_MAP: Record<string, any> = {
|
||||||
|
Sparkles,
|
||||||
|
Activity,
|
||||||
|
ShieldCheck,
|
||||||
|
Heart,
|
||||||
|
Zap,
|
||||||
|
Flame,
|
||||||
|
Star,
|
||||||
|
Users
|
||||||
|
};
|
||||||
|
import { toPersian, cn } from "../lib/utils";
|
||||||
|
import { Product, PRODUCTS } from "../data/products";
|
||||||
|
import { SCIENTIFIC_TERMS } from "../data/scientificTerms";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { useCartStore } from "../store/cartStore";
|
||||||
|
import { usePetStore } from "../store/usePetStore";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface CalculatorState {
|
||||||
|
petType: "young" | "adult";
|
||||||
|
weight: number;
|
||||||
|
setPetType: (type: "young" | "adult") => void;
|
||||||
|
setWeight: (weight: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCalculatorStore = create<CalculatorState>((set) => ({
|
||||||
|
petType: "young",
|
||||||
|
weight: 10,
|
||||||
|
setPetType: (petType) => set({ petType }),
|
||||||
|
setWeight: (weight) => set({ weight }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import Tooltip from "./Tooltip";
|
||||||
|
import SafeImage from "./SafeImage";
|
||||||
|
|
||||||
|
export default function ProductPage({ product, onBack, onProductClick, onWikiNavigate, onShopNavigate }: {
|
||||||
|
product: Product;
|
||||||
|
onBack: () => void;
|
||||||
|
onProductClick: (p: Product) => void;
|
||||||
|
onWikiNavigate?: (id: string) => void;
|
||||||
|
onShopNavigate?: (category?: string) => void;
|
||||||
|
}) {
|
||||||
|
const [activeTab, setActiveTab] = useState<"specs" | "feeding" | "notes">("specs");
|
||||||
|
const [showRefillModal, setShowRefillModal] = useState(false);
|
||||||
|
const [itemQuantity, setItemQuantity] = useState(1);
|
||||||
|
const { pets, getActivePet } = usePetStore();
|
||||||
|
const activePet = getActivePet();
|
||||||
|
const { petType, weight, setPetType, setWeight } = useCalculatorStore();
|
||||||
|
const { addItem } = useCartStore();
|
||||||
|
|
||||||
|
// Re-hydrate product to ensure methods like calculateDosage exist
|
||||||
|
const fullProduct = useMemo(() => {
|
||||||
|
return PRODUCTS.find(p => p.id === product.id) || product;
|
||||||
|
}, [product]);
|
||||||
|
|
||||||
|
const syncActivePet = useMemo(() => {
|
||||||
|
if (activePet) {
|
||||||
|
setWeight(activePet.weight);
|
||||||
|
setPetType(activePet.age <= 1 ? "young" : "adult");
|
||||||
|
}
|
||||||
|
}, [activePet, setWeight, setPetType]);
|
||||||
|
|
||||||
|
const calculation = useMemo(() => {
|
||||||
|
const result = fullProduct.calculateDosage(weight, petType === "young");
|
||||||
|
const duration = Math.floor(fullProduct.packageSize / result.quantity);
|
||||||
|
|
||||||
|
// Smart quantity suggestion: if duration is less than 30 days, suggest 2 packs
|
||||||
|
const suggestedQty = (duration < 30 && duration > 0) ? 2 : 1;
|
||||||
|
setItemQuantity(suggestedQty);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dailyDose: result.quantity,
|
||||||
|
duration: duration || 0,
|
||||||
|
unit: result.unit,
|
||||||
|
description: result.description
|
||||||
|
};
|
||||||
|
}, [fullProduct, petType, weight]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-medical-gray-50 pt-2 pb-20 px-4 md:px-0" dir="rtl">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Mobile Header with Back Button */}
|
||||||
|
<div className="flex items-center justify-between mb-6 md:hidden">
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="p-3 bg-white border border-medical-gray-200 rounded-2xl text-medical-gray-700 shadow-sm flex items-center gap-2 font-vazir text-sm font-bold"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
بازگشت
|
||||||
|
</button>
|
||||||
|
<div className="text-[10px] font-black text-canina-blue bg-canina-blue/5 px-4 py-2 rounded-xl font-vazir whitespace-nowrap">
|
||||||
|
کانینا ایران
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Breadcrumbs */}
|
||||||
|
<nav className="hidden md:flex items-center gap-2 text-xs font-bold text-medical-gray-400 mb-8 overflow-x-auto whitespace-nowrap py-2">
|
||||||
|
<button
|
||||||
|
onClick={() => onBack()}
|
||||||
|
className="hover:text-canina-blue transition-colors font-vazir cursor-pointer"
|
||||||
|
>
|
||||||
|
خانه
|
||||||
|
</button>
|
||||||
|
<ChevronLeft className="w-3 h-3 flex-shrink-0" />
|
||||||
|
<button
|
||||||
|
onClick={() => onShopNavigate?.(product.category)}
|
||||||
|
className="hover:text-canina-blue cursor-pointer font-vazir"
|
||||||
|
>
|
||||||
|
{product.category}
|
||||||
|
</button>
|
||||||
|
<ChevronLeft className="w-3 h-3 flex-shrink-0" />
|
||||||
|
<span className="text-canina-blue font-vazir whitespace-nowrap">{product.name}</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="grid lg:grid-cols-12 gap-12 items-start">
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<div className="lg:col-span-8 space-y-12">
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||||
|
<div className="px-3 py-1 bg-canina-blue text-white rounded-full text-[10px] font-black uppercase tracking-widest font-vazir whitespace-nowrap">
|
||||||
|
استاندارد صنعتی آلمان
|
||||||
|
</div>
|
||||||
|
{product.specialBadge && (
|
||||||
|
<div className="px-3 py-1 bg-medical-gray-900 text-white rounded-full text-[10px] font-black uppercase tracking-widest font-vazir whitespace-nowrap">
|
||||||
|
{product.specialBadge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl lg:text-6xl font-black text-medical-gray-900 leading-tight mb-4 italic font-vazir">
|
||||||
|
{product.name}
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-canina-blue font-bold italic opacity-70 leading-relaxed max-w-2xl font-vazir mb-6">
|
||||||
|
{pets.length > 0 ? (
|
||||||
|
product.optimisticTemplate?.replace("[PetName]", activePet?.name || "").replace("[OnSet]", product.onSetOfAction || "به زودی")
|
||||||
|
) : (
|
||||||
|
product.scientificTagline
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{fullProduct.shortDescription && (
|
||||||
|
<p className="text-lg text-medical-gray-600 font-medium leading-relaxed max-w-3xl font-vazir border-r-4 border-canina-blue pr-6 py-2 bg-canina-blue/5 rounded-l-2xl">
|
||||||
|
{fullProduct.shortDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Key Benefits Section */}
|
||||||
|
{fullProduct.keyBenefits && (
|
||||||
|
<section className="space-y-8">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 bg-canina-blue text-white rounded-2xl flex items-center justify-center shadow-lg">
|
||||||
|
<Star className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl font-black text-medical-gray-900 italic font-vazir">چرا {fullProduct.name}؟</h3>
|
||||||
|
</div>
|
||||||
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
|
{fullProduct.keyBenefits.map((benefit, i) => {
|
||||||
|
const Icon = ICON_MAP[benefit.icon] || CheckCircle2;
|
||||||
|
return (
|
||||||
|
<div key={i} className="bg-white border border-medical-gray-100 rounded-[2rem] p-8 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all group overflow-hidden relative">
|
||||||
|
<div className="absolute -top-10 -left-10 w-24 h-24 bg-canina-blue/5 rounded-full group-hover:scale-150 transition-transform duration-700" />
|
||||||
|
<div className="w-12 h-12 bg-medical-gray-50 rounded-2xl flex items-center justify-center text-canina-blue mb-6 group-hover:bg-canina-blue group-hover:text-white transition-all">
|
||||||
|
<Icon className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h4 className="text-xl font-black text-medical-gray-900 mb-2 font-vazir">{benefit.title}</h4>
|
||||||
|
<p className="text-sm text-medical-gray-500 font-bold font-vazir">{benefit.description}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="bg-white rounded-[3.5rem] p-12 border border-medical-gray-200 shadow-xl relative group overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="absolute top-6 left-6 flex flex-col gap-2 z-10">
|
||||||
|
<div className="px-3 py-1 bg-green-50 text-green-600 rounded-full text-[10px] font-black uppercase tracking-widest border border-green-100 flex items-center gap-1 font-vazir">
|
||||||
|
<CheckCircle2 className="w-3 h-3" />
|
||||||
|
موجود در انبار
|
||||||
|
</div>
|
||||||
|
<div className="px-3 py-1 bg-canina-blue/5 text-canina-blue rounded-full text-[10px] font-black uppercase tracking-widest border border-canina-blue/10 font-vazir whitespace-nowrap">
|
||||||
|
گرید دارویی اختصاصی
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SafeImage
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-full max-w-md mx-auto h-auto drop-shadow-2xl group-hover:scale-105 transition-transform duration-700"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Ingredients Section */}
|
||||||
|
<section className="space-y-8">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 bg-medical-gray-900 text-white rounded-2xl flex items-center justify-center shadow-lg">
|
||||||
|
<FlaskConical className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl font-black text-medical-gray-900 italic font-vazir">ترکیبات و آنالیز علمی</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-10">
|
||||||
|
{/* Ingredients Cards */}
|
||||||
|
<div className="bg-white rounded-[2.5rem] p-8 border border-medical-gray-200">
|
||||||
|
<h4 className="text-xs font-black text-medical-gray-400 uppercase tracking-widest mb-6 font-vazir">مواد تشکیلدهنده برتر</h4>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
{product.main_ingredients.map((ing, idx) => {
|
||||||
|
const termKey = Object.keys(SCIENTIFIC_TERMS).find(key => ing.includes(key));
|
||||||
|
return (
|
||||||
|
<div key={idx} className="bg-medical-gray-50 border border-medical-gray-100 px-5 py-3 rounded-2xl flex items-center gap-3 group hover:border-canina-blue/30 transition-all">
|
||||||
|
<span className="text-sm font-bold text-medical-gray-700 font-vazir">
|
||||||
|
{termKey ? (
|
||||||
|
<Tooltip termKey={termKey} onWikiNavigate={onWikiNavigate}>
|
||||||
|
{ing}
|
||||||
|
</Tooltip>
|
||||||
|
) : ing}
|
||||||
|
</span>
|
||||||
|
<Sparkles className="w-3 h-3 text-canina-blue opacity-40 group-hover:opacity-100 transition-opacity" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Analysis Cards Grid */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-4 font-vazir">تخمین اتمام بسته (هوش مصنوعی)</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{Object.entries(product.analysis).map(([key, value]) => (
|
||||||
|
<div key={key} className="bg-canina-blue text-white p-8 rounded-[2rem] border-b-2 border-white/20 shadow-xl group hover:scale-[1.02] transition-all duration-300">
|
||||||
|
<div className="text-[10px] font-black text-white/80 uppercase tracking-widest mb-3 font-vazir whitespace-nowrap">{key}</div>
|
||||||
|
<div className="text-3xl font-black font-vazir tracking-tighter text-white">{toPersian(value)}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Feeding Section */}
|
||||||
|
<section className="bg-canina-blue/5 border border-canina-blue/10 rounded-[3rem] p-10">
|
||||||
|
<div className="flex items-center gap-4 mb-8">
|
||||||
|
<div className="w-10 h-10 bg-canina-blue text-white rounded-xl flex items-center justify-center">
|
||||||
|
<CalendarDays className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-black text-medical-gray-900 italic font-vazir">توصیه غذایی و نحوه مصرف</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-medical-gray-700 leading-relaxed text-sm md:text-base font-medium font-vazir">
|
||||||
|
{product.feedingAdvice}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Specialist Note */}
|
||||||
|
<section className="bg-white rounded-[3rem] p-10 border border-medical-gray-200">
|
||||||
|
<div className="flex flex-col md:flex-row gap-10 items-start">
|
||||||
|
<img
|
||||||
|
src={product.specialist.image}
|
||||||
|
alt={product.specialist.name}
|
||||||
|
className="w-32 h-32 rounded-[2.5rem] object-cover border-4 border-medical-gray-50 shadow-xl"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="inline-flex items-center gap-2 px-3 py-1 bg-medical-gray-900 text-white rounded-full text-[10px] font-black uppercase tracking-widest mb-4 font-vazir whitespace-nowrap">
|
||||||
|
<Stethoscope className="w-3 h-3" />
|
||||||
|
مورد تأیید دامپزشکان
|
||||||
|
</div>
|
||||||
|
<h4 className="text-2xl font-black text-medical-gray-900 mb-1 font-vazir">{product.specialist.name}</h4>
|
||||||
|
<p className="text-sm font-bold text-canina-blue mb-6 font-vazir">{product.specialist.title}</p>
|
||||||
|
<p className="text-base text-medical-gray-600 leading-relaxed italic font-vazir">
|
||||||
|
"{product.specialist.message}"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Specialized Landing Banner */}
|
||||||
|
<section>
|
||||||
|
<div
|
||||||
|
className="relative bg-canina-blue rounded-[3rem] p-12 overflow-hidden group cursor-pointer"
|
||||||
|
onClick={() => toast.info("بزودی: صفحه فرود تخصصی این محصول در حال آمادهسازی است")}
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 right-0 w-full h-full bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-10" />
|
||||||
|
<div className="absolute -bottom-20 -left-20 w-80 h-80 bg-white/10 rounded-full blur-3xl group-hover:bg-white/20 transition-all duration-700" />
|
||||||
|
|
||||||
|
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-8">
|
||||||
|
<div className="text-center md:text-right">
|
||||||
|
<h3 className="text-3xl lg:text-4xl font-black text-white mb-4 italic">مشاهده بررسی تخصصی و نتایج درمانی</h3>
|
||||||
|
<p className="text-white/80 font-bold font-vazir max-w-lg">
|
||||||
|
گزارشهای علمی، ویدیوهای آموزشی و تجربیات واقعی دیگر صاحبان پت در لندینگپیج اختصاصی این محصول.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white text-canina-blue px-8 py-4 rounded-2xl font-black shadow-2xl hover:scale-105 transition-transform flex items-center gap-3">
|
||||||
|
ورود به آزمایشگاه علمی
|
||||||
|
<ArrowRight className="w-5 h-5 rotate-180" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* FAQ Section */}
|
||||||
|
{product.faqs && (
|
||||||
|
<section className="pt-10">
|
||||||
|
<div className="flex items-center gap-4 mb-10">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 text-canina-blue rounded-2xl flex items-center justify-center">
|
||||||
|
<Info className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl font-black text-medical-gray-900 italic font-vazir">پاسخ به ابهامات شما</h3>
|
||||||
|
</div>
|
||||||
|
<div className="grid md:grid-cols-1 gap-4">
|
||||||
|
{product.faqs.map((faq, i) => (
|
||||||
|
<details key={i} className="group bg-white border border-medical-gray-200 rounded-3xl overflow-hidden hover:border-canina-blue/30 transition-all">
|
||||||
|
<summary className="flex items-center justify-between p-6 cursor-pointer list-none">
|
||||||
|
<span className="font-bold text-medical-gray-900 font-vazir">{faq.question}</span>
|
||||||
|
<ChevronDown className="w-5 h-5 text-canina-blue group-open:rotate-180 transition-transform" />
|
||||||
|
</summary>
|
||||||
|
<div className="px-6 pb-6 text-sm text-medical-gray-500 font-medium leading-relaxed font-vazir border-t border-medical-gray-50 pt-4">
|
||||||
|
{faq.answer}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sticky Sidebar */}
|
||||||
|
<div className="lg:col-span-4 lg:sticky lg:top-24 space-y-8">
|
||||||
|
<div className="bg-white rounded-[3rem] border-2 border-medical-gray-100 p-8 shadow-2xl relative overflow-hidden">
|
||||||
|
<div className="absolute top-0 right-0 w-24 h-24 bg-canina-blue/5 rounded-full -mr-12 -mt-12" />
|
||||||
|
|
||||||
|
<div className="text-4xl font-black font-vazir text-medical-gray-900 tracking-tighter mb-4">
|
||||||
|
{toPersian(product.price)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Refill Auto-Calculator */}
|
||||||
|
<div className="bg-canina-blue rounded-[2rem] p-6 text-white mb-8 border-b-8 border-white/20 shadow-2xl">
|
||||||
|
<div className="flex items-center gap-2 mb-4 opacity-90">
|
||||||
|
<Clock className="w-4 h-4 text-white" />
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-widest font-vazir text-white">هوش مصنوعی تکرار خرید</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-2xl font-black font-vazir text-white">{toPersian(calculation.duration)} روز</div>
|
||||||
|
<div className="text-[9px] font-bold text-white/80 font-vazir whitespace-nowrap">تخمین اتمام برای {toPersian(weight)} کیلوگرم</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-xl font-black font-vazir text-white">{toPersian(calculation.dailyDose)}</div>
|
||||||
|
<div className="text-[9px] font-bold text-white/80 font-vazir whitespace-nowrap">دوز روزانه ({calculation.unit === 'g' ? 'گرم' : calculation.unit})</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Manual Calculator (Mini) */}
|
||||||
|
<div className="bg-medical-gray-50 rounded-2xl p-6 border border-medical-gray-100 mb-8 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-[10px] font-black text-medical-gray-400 font-vazir">وزن پت (کیلوگرم)</span>
|
||||||
|
<span className="text-sm font-black text-canina-blue font-vazir">{toPersian(weight)}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1" max="100"
|
||||||
|
value={weight}
|
||||||
|
onChange={(e) => setWeight(parseInt(e.target.value))}
|
||||||
|
className="w-full h-1.5 bg-medical-gray-200 rounded-lg appearance-none cursor-pointer accent-canina-blue"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button onClick={() => setPetType("young")} className={`flex-1 py-2 rounded-xl text-[10px] font-black transition-all ${petType === "young" ? 'bg-canina-blue text-white shadow-lg' : 'bg-white text-medical-gray-400'}`}>جوان</button>
|
||||||
|
<button onClick={() => setPetType("adult")} className={`flex-1 py-2 rounded-xl text-[10px] font-black transition-all ${petType === "adult" ? 'bg-canina-blue text-white shadow-lg' : 'bg-white text-medical-gray-400'}`}>بالغ</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 mb-6">
|
||||||
|
{/* Quantity Selector - 40% Width */}
|
||||||
|
<div className="flex items-center gap-4 bg-medical-gray-50 border border-medical-gray-100 rounded-2xl p-2 h-16 w-[40%]">
|
||||||
|
<button
|
||||||
|
onClick={() => setItemQuantity(Math.max(1, itemQuantity - 1))}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-white text-medical-gray-900 border border-medical-gray-200 hover:bg-canina-blue hover:text-white hover:border-canina-blue transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
<Minus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<span className="text-xl font-black text-medical-gray-900 flex-1 text-center font-vazir">
|
||||||
|
{toPersian(itemQuantity)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setItemQuantity(itemQuantity + 1)}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-white text-medical-gray-900 border border-medical-gray-200 hover:bg-canina-blue hover:text-white hover:border-canina-blue transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add to Cart Button - 60% Width */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
addItem(product, itemQuantity, { quantity: calculation.dailyDose, unit: calculation.unit });
|
||||||
|
toast.success(`${toPersian(itemQuantity)} عدد ${product.name} به سبد خرید اضافه شد`);
|
||||||
|
}}
|
||||||
|
className="w-[60%] flex items-center justify-center gap-1.5 px-4 bg-medical-gray-900 text-white rounded-2xl h-16 font-black text-sm hover:bg-canina-blue transition-all shadow-xl shadow-black/10 font-vazir whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<ShoppingBag className="w-5 h-5 mb-1" />
|
||||||
|
افزودن به سبد خرید
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex flex-col gap-3">
|
||||||
|
<div className="flex items-center gap-2 text-[10px] font-bold text-medical-gray-400 font-vazir">
|
||||||
|
<ShieldCheck className="w-4 h-4 text-green-500" />
|
||||||
|
ضمانت اصالت محصول (Made in Germany)
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-[10px] font-bold text-medical-gray-400 font-vazir">
|
||||||
|
<Clock className="w-4 h-4 text-canina-blue" />
|
||||||
|
آماده ارسال (تحویل حداکثر ۴۸ ساعت)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* expected results box */}
|
||||||
|
{product.expectedResults && (
|
||||||
|
<div className="bg-white rounded-[2.5rem] border border-medical-gray-200 p-8">
|
||||||
|
<h4 className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-6 font-vazir">تغییرات قابل انتظار</h4>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{product.expectedResults.map((res, i) => {
|
||||||
|
const Icon = ICON_MAP[res.icon] || Sparkles;
|
||||||
|
return (
|
||||||
|
<div key={i} className="flex items-center gap-4 group">
|
||||||
|
<div className="w-10 h-10 bg-medical-gray-50 rounded-xl flex items-center justify-center text-canina-blue group-hover:bg-canina-blue group-hover:text-white transition-all">
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-bold text-medical-gray-700 font-vazir">{res.text}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Smart Cross-sell: Recommended Combinations */}
|
||||||
|
{product.relatedProducts && product.relatedProducts.length > 0 && (
|
||||||
|
<section className="mt-32 pt-20 border-t border-medical-gray-200">
|
||||||
|
<div className="flex flex-col items-center text-center mb-16 px-4">
|
||||||
|
<div className="inline-flex items-center gap-2 px-4 py-2 bg-canina-blue/5 border border-canina-blue/10 rounded-full text-canina-blue text-xs font-black uppercase tracking-widest mb-6 font-vazir whitespace-nowrap">
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
فرمولاسیون ترکیبی همافزا (Synergy Blend)
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl lg:text-5xl font-black text-medical-gray-900 leading-tight">
|
||||||
|
اثربخشی <span className="text-canina-blue italic">دوبرابر</span> با ترکیب هوشمند
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid lg:grid-cols-2 gap-8">
|
||||||
|
{product.relatedProducts.map(relId => {
|
||||||
|
const relProduct = PRODUCTS.find(p => p.id === relId);
|
||||||
|
if (!relProduct) return null;
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={relId}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="bg-white rounded-[3rem] p-8 border border-medical-gray-200 flex flex-col md:flex-row gap-8 items-center cursor-pointer hover:shadow-2xl transition-all"
|
||||||
|
onClick={() => onProductClick(relProduct)}
|
||||||
|
>
|
||||||
|
<div className="w-40 h-40 bg-medical-gray-50 rounded-[2rem] p-4 flex items-center justify-center">
|
||||||
|
<SafeImage src={relProduct.image} alt={relProduct.name} className="w-full h-full object-contain drop-shadow-xl" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-center md:text-right">
|
||||||
|
<h4 className="text-xl font-black text-medical-gray-900 mb-2">{relProduct.name}</h4>
|
||||||
|
<p className="text-sm text-medical-gray-500 mb-6">{relProduct.description}</p>
|
||||||
|
<div className="flex items-center justify-center md:justify-start gap-4">
|
||||||
|
<span className="text-lg font-black text-canina-blue">{relProduct.price}</span>
|
||||||
|
<ArrowRight className="w-4 h-4 text-medical-gray-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-px h-24 bg-medical-gray-100 hidden md:block" />
|
||||||
|
<div className="flex items-center justify-center gap-1 group">
|
||||||
|
<span className="text-xs font-black text-medical-gray-400 group-hover:text-canina-blue transition-colors">مشاهده محصول مکمل</span>
|
||||||
|
<ChevronLeft className="w-4 h-4 text-medical-gray-300 group-hover:text-canina-blue group-hover:-translate-x-1 transition-all rtl:group-hover:translate-x-1" />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Refill Automation Popup */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showRefillModal && (
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={() => setShowRefillModal(false)}
|
||||||
|
className="absolute inset-0 bg-medical-gray-900/60 backdrop-blur-md"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
className="bg-white w-full max-w-lg rounded-[3rem] p-10 relative z-10 shadow-2xl overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-canina-blue via-blue-400 to-canina-blue" />
|
||||||
|
<button
|
||||||
|
onClick={() => setShowRefillModal(false)}
|
||||||
|
className="absolute top-6 left-6 text-medical-gray-400 hover:text-medical-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="w-20 h-20 bg-canina-blue/10 rounded-[2rem] flex items-center justify-center mx-auto mb-8">
|
||||||
|
<Bell className="w-10 h-10 text-canina-blue animate-bounce" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-3xl font-black text-medical-gray-900 mb-4 leading-tight italic">سیستم یادآوری هوشمند</h3>
|
||||||
|
<div className="bg-medical-gray-50 border border-medical-gray-100 rounded-3xl p-6 mb-8 text-right">
|
||||||
|
<p className="text-medical-gray-700 leading-relaxed font-medium font-vazir">
|
||||||
|
بر اساس وزن <span className="text-canina-blue font-bold">{toPersian(weight)} کیلوگرمی</span> {activePet ? `برای ${activePet.name}` : 'سگ شما'}، این بسته {toPersian(product.packageSize)} تایی دقیقاً <span className="text-canina-blue font-bold">{toPersian(calculation.duration)} روز</span> دیگر تمام میشود.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 flex items-center gap-2 text-xs font-bold text-medical-gray-500 font-vazir">
|
||||||
|
<ShieldCheck className="w-4 h-4 text-green-500" />
|
||||||
|
آیا مایلید سیستم ۵ روز قبل از اتمام، به شما پیامک یادآوری ارسال کند؟
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowRefillModal(false)}
|
||||||
|
className="w-full bg-canina-blue text-white py-5 rounded-2xl font-black text-lg hover:shadow-xl hover:shadow-canina-blue/20 transition-all flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-5 h-5" />
|
||||||
|
بله، پیامک یادآوری فعال شود
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowRefillModal(false)}
|
||||||
|
className="w-full py-4 text-medical-gray-400 font-bold text-sm hover:text-medical-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
خیر، فقط محصول را به سبد خرید اضافه کن
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
src/components/SafeImage.tsx
Normal file
73
src/components/SafeImage.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ImageOff, Sparkles } from "lucide-react";
|
||||||
|
import { cn } from "../lib/utils";
|
||||||
|
|
||||||
|
interface SafeImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||||
|
src?: string;
|
||||||
|
alt?: string;
|
||||||
|
className?: string;
|
||||||
|
imgClassName?: string;
|
||||||
|
fallbackText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SafeImage({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
className,
|
||||||
|
imgClassName,
|
||||||
|
fallbackText = "در حال بروزرسانی تصویر...",
|
||||||
|
...props
|
||||||
|
}: SafeImageProps) {
|
||||||
|
const [error, setError] = React.useState(false);
|
||||||
|
const [loading, setLoading] = React.useState(true);
|
||||||
|
|
||||||
|
if (!src || error) {
|
||||||
|
return (
|
||||||
|
<div className={cn(
|
||||||
|
"flex flex-col items-center justify-center bg-white border border-medical-gray-100 p-4 text-center min-h-[120px] rounded-3xl shadow-inner relative overflow-hidden",
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
{/* Subtle Brand Pattern Background */}
|
||||||
|
<div className="absolute inset-0 opacity-[0.03] pointer-events-none">
|
||||||
|
<div className="grid grid-cols-6 h-full w-full">
|
||||||
|
{Array.from({length: 24}).map((_, i) => (
|
||||||
|
<div key={i} className="border border-canina-blue" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-16 h-16 bg-canina-blue rounded-2xl flex items-center justify-center text-white font-black text-3xl mb-4 shadow-xl shadow-canina-blue/20 italic transform -rotate-6">C</div>
|
||||||
|
<div className="relative">
|
||||||
|
<ImageOff className="w-5 h-5 text-medical-gray-200 mb-2" />
|
||||||
|
<Sparkles className="w-3 h-3 text-canina-blue absolute -top-1 -right-1 animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<span className="text-[9px] font-black text-canina-blue/60 uppercase tracking-[0.2em] font-vazir leading-relaxed">
|
||||||
|
{fallbackText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("relative overflow-hidden group flex items-center justify-center", className)}>
|
||||||
|
{loading && (
|
||||||
|
<div className="absolute inset-0 bg-medical-gray-50 animate-pulse flex items-center justify-center z-10">
|
||||||
|
<Sparkles className="w-6 h-6 text-canina-blue/20" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className={cn(
|
||||||
|
"transition-all duration-700 w-full h-full",
|
||||||
|
imgClassName,
|
||||||
|
loading ? "opacity-0 scale-110" : "opacity-100 scale-100"
|
||||||
|
)}
|
||||||
|
onLoad={() => setLoading(false)}
|
||||||
|
onError={() => setError(true)}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
src/components/SearchResultsPage.tsx
Normal file
129
src/components/SearchResultsPage.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { Search, ShoppingBag, ChevronLeft, ArrowRight, Activity, Sparkles, HeartPulse, Stethoscope, ShieldCheck, Heart, AlertCircle } from "lucide-react";
|
||||||
|
import { usePetStore } from "../store/usePetStore";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { Product, PRODUCTS } from "../data/products";
|
||||||
|
|
||||||
|
export default function SearchResultsPage({ query, onProductClick, onBack }: { query: string; onProductClick: (p: Product) => void; onBack: () => void }) {
|
||||||
|
const { getActivePet } = usePetStore();
|
||||||
|
const activePet = getActivePet();
|
||||||
|
|
||||||
|
const results = PRODUCTS.filter(p =>
|
||||||
|
p.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
p.description.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
p.main_ingredients.some(ing => ing.toLowerCase().includes(query.toLowerCase())) ||
|
||||||
|
p.symptoms.some(sym => sym.toLowerCase().includes(query.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
|
const bestSellers = PRODUCTS.slice(0, 3);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-medical-gray-50 py-20 px-6 font-vazir" dir="rtl">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<button onClick={onBack} className="flex items-center gap-2 text-medical-gray-400 font-black mb-10 hover:text-canina-blue transition-colors">
|
||||||
|
<ArrowRight className="w-5 h-5 rotate-180" />
|
||||||
|
بازگشت
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex flex-col md:flex-row md:items-end justify-between mb-16 gap-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-4xl font-black text-medical-gray-900 italic mb-4">
|
||||||
|
نتایج جستجو برای: <span className="text-canina-blue">"{query}"</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-medical-gray-500 font-medium">
|
||||||
|
{results.length} محصول با معیارهای شما مطابقت دارد.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{results.length > 0 ? (
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{results.map((product) => {
|
||||||
|
const compatibility = (() => {
|
||||||
|
if (!activePet) return null;
|
||||||
|
const sameSpecies = product.suitableFor === activePet.type || product.suitableFor === "هر دو";
|
||||||
|
const helpsSymptom = activePet.medicalConditions.some(c => product.symptoms.includes(c));
|
||||||
|
if (!sameSpecies) return { type: 'alert', text: `مخصوص ${product.suitableFor}` };
|
||||||
|
if (helpsSymptom) return { type: 'success', text: `توصیه شده برای ${activePet.name}` };
|
||||||
|
return { type: 'neutral', text: `مناسب برای ${activePet.name}` };
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={product.id}
|
||||||
|
layoutId={product.id}
|
||||||
|
onClick={() => onProductClick(product)}
|
||||||
|
className="bg-white rounded-[2.5rem] p-8 border border-medical-gray-200 group cursor-pointer hover:shadow-2xl hover:shadow-canina-blue/10 transition-all flex flex-col relative"
|
||||||
|
>
|
||||||
|
<div className="relative aspect-square mb-8 bg-medical-gray-50 rounded-[2rem] overflow-hidden p-6 flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
src={product.image}
|
||||||
|
alt={product.name}
|
||||||
|
className="w-full h-full object-contain group-hover:scale-110 transition-transform duration-500"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
{product.specialBadge && (
|
||||||
|
<div className="absolute top-4 right-4 bg-medical-gray-900 text-white text-[8px] font-black px-3 py-1 rounded-full uppercase tracking-widest">
|
||||||
|
{product.specialBadge}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{compatibility && (
|
||||||
|
<div className={`absolute top-14 right-4 z-10 text-[8px] font-black uppercase tracking-widest px-3 py-1 rounded-full shadow-md flex items-center gap-1 ${
|
||||||
|
compatibility.type === 'alert' ? 'bg-amber-100 text-amber-700' :
|
||||||
|
compatibility.type === 'success' ? 'bg-green-100 text-green-700' : 'bg-medical-gray-100 text-medical-gray-600'
|
||||||
|
}`}>
|
||||||
|
{compatibility.type === 'alert' ? <AlertCircle className="w-2.5 h-2.5" /> : compatibility.type === 'success' ? <Heart className="w-2.5 h-2.5" /> : <Sparkles className="w-2.5 h-2.5" />}
|
||||||
|
{compatibility.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<span className="text-[10px] font-black text-canina-blue uppercase tracking-widest mb-2 block">{product.category}</span>
|
||||||
|
<h3 className="text-xl font-black text-medical-gray-900 mb-2 leading-tight group-hover:text-canina-blue transition-colors">{product.name}</h3>
|
||||||
|
<p className="text-xs text-medical-gray-400 line-clamp-2 leading-relaxed mb-6">{product.benefits}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-6 border-t border-medical-gray-100 italic">
|
||||||
|
<div>
|
||||||
|
<span className="text-lg font-black text-medical-gray-900">{product.price}</span>
|
||||||
|
</div>
|
||||||
|
<button className="bg-medical-gray-900 text-white w-10 h-10 rounded-xl flex items-center justify-center group-hover:bg-canina-blue transition-colors">
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-16">
|
||||||
|
<div className="bg-white border border-medical-gray-200 rounded-[3rem] p-12 text-center shadow-sm">
|
||||||
|
<div className="w-20 h-20 bg-medical-gray-50 rounded-full flex items-center justify-center mx-auto text-medical-gray-300 mb-6">
|
||||||
|
<Search className="w-10 h-10" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-black text-medical-gray-800 mb-4">نتیجهای یافت نشد</h3>
|
||||||
|
<p className="text-medical-gray-500 font-medium max-w-md mx-auto leading-relaxed">
|
||||||
|
متاسفیم، هیچ محصولی با عبارت مورد نظر شما مطابقت نداشت. شاید محصولات پرفروش ما برای شما مفید باشند؟
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-black text-medical-gray-900 mb-10 italic">محصولات پیشنهادی (پرفروش)</h3>
|
||||||
|
<div className="grid md:grid-cols-3 gap-8 opacity-60 hover:opacity-100 transition-opacity">
|
||||||
|
{bestSellers.map(p => (
|
||||||
|
<div key={p.id} onClick={() => onProductClick(p)} className="cursor-pointer group">
|
||||||
|
<div className="aspect-square bg-white rounded-[2rem] p-6 mb-4 border border-medical-gray-100 flex items-center justify-center">
|
||||||
|
<img src={p.image} className="w-40 h-40 object-contain group-hover:scale-105 transition-transform" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-black text-medical-gray-900 text-center">{p.name}</h4>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
src/components/Skeleton.tsx
Normal file
85
src/components/Skeleton.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface SkeletonProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Skeleton = ({ className }: SkeletonProps) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`bg-medical-gray-100 animate-pulse rounded-2xl relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent ${className}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProductCardSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-[2.5rem] p-6 border border-medical-gray-50 shadow-sm flex flex-col h-full">
|
||||||
|
{/* Image Area */}
|
||||||
|
<Skeleton className="w-full aspect-square mb-6 rounded-[2rem]" />
|
||||||
|
|
||||||
|
{/* Category Tag */}
|
||||||
|
<Skeleton className="w-20 h-4 mb-4 rounded-full" />
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<Skeleton className="w-full h-8 mb-2 rounded-lg" />
|
||||||
|
<Skeleton className="w-2/3 h-8 mb-4 rounded-lg" />
|
||||||
|
|
||||||
|
{/* Symptoms */}
|
||||||
|
<div className="flex gap-2 mb-6">
|
||||||
|
<Skeleton className="w-16 h-6 rounded-full" />
|
||||||
|
<Skeleton className="w-12 h-6 rounded-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-auto pt-6 border-t border-medical-gray-50 flex items-center justify-between">
|
||||||
|
<Skeleton className="w-24 h-8 rounded-lg" />
|
||||||
|
<Skeleton className="w-12 h-12 rounded-2xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PetProfileSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-[3rem] p-10 shadow-2xl border border-medical-gray-50">
|
||||||
|
<div className="flex flex-col md:flex-row items-center gap-10 mb-12">
|
||||||
|
<Skeleton className="w-40 h-40 rounded-[2.5rem]" />
|
||||||
|
<div className="flex-1 w-full md:w-auto text-center md:text-right">
|
||||||
|
<Skeleton className="w-32 h-10 mb-4 mx-auto md:mx-0" />
|
||||||
|
<Skeleton className="w-48 h-6 mb-6 mx-auto md:mx-0" />
|
||||||
|
<div className="flex flex-wrap gap-3 justify-center md:justify-start">
|
||||||
|
<Skeleton className="w-24 h-5 rounded-full" />
|
||||||
|
<Skeleton className="w-24 h-5 rounded-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||||
|
{[1, 2, 3, 4].map(i => (
|
||||||
|
<div key={i} className="bg-medical-gray-50 p-6 rounded-3xl">
|
||||||
|
<Skeleton className="w-20 h-4 mb-4 mx-auto" />
|
||||||
|
<Skeleton className="w-12 h-8 mx-auto" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OrderRowSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-6 p-6 border-b border-medical-gray-100 last:border-0">
|
||||||
|
<Skeleton className="w-16 h-16 rounded-2xl flex-shrink-0" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<Skeleton className="w-40 h-6" />
|
||||||
|
<Skeleton className="w-20 h-4" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Skeleton className="w-32 h-4" />
|
||||||
|
<Skeleton className="w-16 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
282
src/components/SmartAdvisor.tsx
Normal file
282
src/components/SmartAdvisor.tsx
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import {
|
||||||
|
Dog,
|
||||||
|
Cat,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
ShieldCheck,
|
||||||
|
Activity,
|
||||||
|
Bone,
|
||||||
|
Sparkles,
|
||||||
|
Pill,
|
||||||
|
Sparkle
|
||||||
|
} from "lucide-react";
|
||||||
|
import { toPersian, cn } from "../lib/utils";
|
||||||
|
|
||||||
|
interface SmartAdvisorProps {
|
||||||
|
onComplete: (data: {
|
||||||
|
name: string,
|
||||||
|
type: "سگ" | "گربه",
|
||||||
|
breed: string,
|
||||||
|
age: number,
|
||||||
|
weight: number,
|
||||||
|
activityLevel: "کم" | "متوسط" | "زیاد",
|
||||||
|
medicalConditions: string[]
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MEDICAL_OPTIONS = [
|
||||||
|
{ id: "joint_surgery", label: "جراحی مفاصل", condition: "جراحی مفاصل" },
|
||||||
|
{ id: "recent_birth", label: "زایمان اخیر", condition: "زایمان اخیر" },
|
||||||
|
{ id: "pregnancy", label: "بارداری", condition: "بارداری" },
|
||||||
|
{ id: "digestion", label: "مشکلات گوارشی", condition: "مشکلات گوارشی" },
|
||||||
|
{ id: "hair_loss", label: "ریزش موی شدید", condition: "ریزش موی شدید" },
|
||||||
|
{ id: "appetite", label: "بیاشتهایی", condition: "بیاشتهایی" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function SmartAdvisor({ onComplete }: SmartAdvisorProps) {
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
|
||||||
|
// Step 1 Data
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [type, setType] = useState<"سگ" | "گربه">("سگ");
|
||||||
|
const [breed, setBreed] = useState("");
|
||||||
|
|
||||||
|
// Step 2 Data
|
||||||
|
const [age, setAge] = useState<number>(3);
|
||||||
|
const [weight, setWeight] = useState<number>(10);
|
||||||
|
const [activityLevel, setActivityLevel] = useState<"کم" | "متوسط" | "زیاد">("متوسط");
|
||||||
|
|
||||||
|
// Step 3 Data
|
||||||
|
const [medicalConditions, setMedicalConditions] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const handleNext = () => setStep(s => s + 1);
|
||||||
|
const handleBack = () => setStep(s => s - 1);
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
onComplete({
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
breed,
|
||||||
|
age,
|
||||||
|
weight,
|
||||||
|
activityLevel,
|
||||||
|
medicalConditions
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="canina-advisor" className="py-24 bg-white">
|
||||||
|
<div className="max-w-4xl mx-auto px-4">
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
className="inline-flex items-center gap-2 px-4 py-2 bg-canina-blue/5 rounded-full mb-6"
|
||||||
|
>
|
||||||
|
<Sparkle className="w-4 h-4 text-canina-blue" />
|
||||||
|
<span className="text-canina-blue text-xs font-black uppercase tracking-widest font-vazir">تکنولوژی پایش هوشمند</span>
|
||||||
|
</motion.div>
|
||||||
|
<h2 className="text-4xl lg:text-5xl font-black text-medical-gray-900 mb-4 font-vazir italic">دستیار سلامت کانینا</h2>
|
||||||
|
<p className="text-medical-gray-500 font-bold text-lg font-vazir">فقط چند قدم تا صدور شناسنامه هوشمند و دریافت پیشنهادات تخصصی</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-medical-gray-50 rounded-[3.5rem] border border-medical-gray-100 p-8 lg:p-12 shadow-2xl shadow-medical-gray-200/50 relative overflow-hidden">
|
||||||
|
{/* Progress Indicator */}
|
||||||
|
<div className="absolute top-0 left-0 w-full h-2 bg-medical-gray-200">
|
||||||
|
<motion.div
|
||||||
|
className="h-full bg-canina-blue"
|
||||||
|
initial={{ width: "0%" }}
|
||||||
|
animate={{ width: `${(step / 3) * 100}%` }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{step === 1 && (
|
||||||
|
<motion.div
|
||||||
|
key="step1"
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
className="space-y-8"
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-2xl font-black text-medical-gray-900 mb-2 font-vazir uppercase italic tracking-tight">گام اول: هویت بصری</h3>
|
||||||
|
<p className="text-medical-gray-400 font-bold font-vazir">همدم شما رو با چه اسمی صدا میزنید؟</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setType("سگ")}
|
||||||
|
className={`flex flex-col items-center gap-4 p-8 rounded-[2.5rem] border-4 transition-all ${type === "سگ" ? 'border-canina-blue bg-white shadow-xl shadow-canina-blue/10 scale-105' : 'border-transparent bg-white/50 text-medical-gray-300 hover:bg-white'}`}
|
||||||
|
>
|
||||||
|
<Dog className={`w-12 h-12 ${type === "سگ" ? 'text-canina-blue' : 'text-current'}`} />
|
||||||
|
<span className="text-lg font-black font-vazir">سگ</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setType("گربه")}
|
||||||
|
className={`flex flex-col items-center gap-4 p-8 rounded-[2.5rem] border-4 transition-all ${type === "گربه" ? 'border-canina-blue bg-white shadow-xl shadow-canina-blue/10 scale-105' : 'border-transparent bg-white/50 text-medical-gray-300 hover:bg-white'}`}
|
||||||
|
>
|
||||||
|
<Cat className={`w-12 h-12 ${type === "گربه" ? 'text-canina-blue' : 'text-current'}`} />
|
||||||
|
<span className="text-lg font-black font-vazir">گربه</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-black text-medical-gray-400 pr-2">نام پت</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
placeholder="لوسی، تدی..."
|
||||||
|
className="w-full bg-white border border-medical-gray-200 rounded-2xl py-4 px-6 focus:ring-2 focus:ring-canina-blue/20 outline-none font-black text-lg font-vazir"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-xs font-black text-medical-gray-400 pr-2">نژاد</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={breed}
|
||||||
|
onChange={e => setBreed(e.target.value)}
|
||||||
|
placeholder="ژرمن، پرشین..."
|
||||||
|
className="w-full bg-white border border-medical-gray-200 rounded-2xl py-4 px-6 focus:ring-2 focus:ring-canina-blue/20 outline-none font-bold font-vazir"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={!name || !breed}
|
||||||
|
className="w-full py-6 bg-medical-gray-900 text-white rounded-3xl font-black text-lg hover:bg-canina-blue transition-all shadow-xl shadow-medical-gray-900/10 flex items-center justify-center gap-3 font-vazir disabled:opacity-50"
|
||||||
|
>
|
||||||
|
ثبت هویت و ادامه
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 2 && (
|
||||||
|
<motion.div
|
||||||
|
key="step2"
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
className="space-y-8"
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-2xl font-black text-medical-gray-900 mb-2 font-vazir uppercase italic tracking-tight">گام دوم: پایش فیزیکی</h3>
|
||||||
|
<p className="text-medical-gray-400 font-bold font-vazir">اطلاعات فیزیکی دقیق به دوزبندی صحیح مکملها کمک میکند</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="block text-center text-xs font-black text-medical-gray-400 uppercase tracking-widest font-vazir">سن (سال)</label>
|
||||||
|
<div className="flex items-center gap-4 bg-white rounded-3xl p-2 border border-medical-gray-200">
|
||||||
|
<button onClick={() => setAge(Math.max(0, age - 1))} className="w-10 h-10 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white transition-all shadow-sm">-</button>
|
||||||
|
<span className="flex-1 text-center text-2xl font-black text-canina-blue font-vazir">{toPersian(age.toString())}</span>
|
||||||
|
<button onClick={() => setAge(age + 1)} className="w-10 h-10 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white transition-all shadow-sm">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="block text-center text-xs font-black text-medical-gray-400 uppercase tracking-widest font-vazir">وزن (کیلوگرم)</label>
|
||||||
|
<div className="flex items-center gap-4 bg-white rounded-3xl p-2 border border-medical-gray-200">
|
||||||
|
<button onClick={() => setWeight(Math.max(1, weight - 1))} className="w-10 h-10 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white transition-all shadow-sm">-</button>
|
||||||
|
<span className="flex-1 text-center text-2xl font-black text-canina-blue font-vazir">{toPersian(weight.toString())}</span>
|
||||||
|
<button onClick={() => setWeight(weight + 1)} className="w-10 h-10 flex items-center justify-center bg-medical-gray-50 rounded-xl text-medical-gray-900 hover:bg-canina-blue hover:text-white transition-all shadow-sm">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="block text-center text-xs font-black text-medical-gray-400 uppercase tracking-widest font-vazir">سطح فعالیت روزانه</label>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
{["کم", "متوسط", "زیاد"].map(level => (
|
||||||
|
<button
|
||||||
|
key={level}
|
||||||
|
onClick={() => setActivityLevel(level as any)}
|
||||||
|
className={`py-4 rounded-2xl border-2 transition-all font-black italic ${activityLevel === level ? 'border-canina-blue bg-white text-canina-blue shadow-lg' : 'border-transparent bg-white/50 text-medical-gray-400 hover:bg-white'}`}
|
||||||
|
>
|
||||||
|
{level}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
className="flex-1 py-6 bg-medical-gray-100 text-medical-gray-500 rounded-3xl font-black text-lg hover:bg-medical-gray-200 transition-all flex items-center justify-center gap-3 font-vazir"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
قبلی
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleNext}
|
||||||
|
className="flex-[2] py-6 bg-medical-gray-900 text-white rounded-3xl font-black text-lg hover:bg-canina-blue transition-all shadow-xl shadow-medical-gray-900/10 flex items-center justify-center gap-3 font-vazir"
|
||||||
|
>
|
||||||
|
گام نهایی سلامت
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 3 && (
|
||||||
|
<motion.div
|
||||||
|
key="step3"
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
className="space-y-8"
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-2xl font-black text-medical-gray-900 mb-2 font-vazir uppercase italic tracking-tight">گام آخر: سوابق و حساسیتها</h3>
|
||||||
|
<p className="text-medical-gray-400 font-bold font-vazir">در صورت وجود هر یک از موارد زیر، آن را تیک بزنید</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{MEDICAL_OPTIONS.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.id}
|
||||||
|
onClick={() => {
|
||||||
|
setMedicalConditions(prev =>
|
||||||
|
prev.includes(opt.condition) ? prev.filter(c => c !== opt.condition) : [...prev, opt.condition]
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className={`flex items-center gap-4 p-5 rounded-2xl border-2 transition-all text-right ${medicalConditions.includes(opt.condition) ? 'border-canina-blue bg-white shadow-md' : 'border-transparent bg-white/50 text-medical-gray-500 hover:bg-white'}`}
|
||||||
|
>
|
||||||
|
<div className={`w-8 h-8 rounded-lg flex items-center justify-center border ${medicalConditions.includes(opt.condition) ? 'bg-canina-blue border-canina-blue text-white' : 'border-medical-gray-200 text-transparent'}`}>
|
||||||
|
<ShieldCheck className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<span className={`text-sm font-black font-vazir ${medicalConditions.includes(opt.condition) ? 'text-canina-blue' : 'text-medical-gray-700'}`}>{opt.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
className="flex-1 py-6 bg-medical-gray-100 text-medical-gray-500 rounded-3xl font-black text-lg hover:bg-medical-gray-200 transition-all flex items-center justify-center gap-3 font-vazir"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
قبلی
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="flex-[2] py-6 bg-canina-blue text-white rounded-3xl font-black text-lg hover:bg-indigo-700 transition-all shadow-xl shadow-canina-blue/20 flex items-center justify-center gap-3 font-vazir"
|
||||||
|
>
|
||||||
|
تکمیل و صدور شناسنامه
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
src/components/Tooltip.tsx
Normal file
59
src/components/Tooltip.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
import { Info, ExternalLink } from 'lucide-react';
|
||||||
|
import { SCIENTIFIC_TERMS } from '../data/scientificTerms';
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
termKey: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
onWikiNavigate?: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Tooltip({ termKey, children, onWikiNavigate }: TooltipProps) {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const termData = SCIENTIFIC_TERMS[termKey];
|
||||||
|
|
||||||
|
if (!termData) return <>{children}</>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="relative inline-block group cursor-help border-b border-dotted border-canina-blue/40"
|
||||||
|
onMouseEnter={() => setIsVisible(true)}
|
||||||
|
onMouseLeave={() => setIsVisible(false)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{isVisible && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
|
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 w-64 p-4 bg-medical-gray-900 text-white rounded-2xl shadow-2xl z-[100] text-sm"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2 text-canina-blue">
|
||||||
|
<Info className="w-4 h-4" />
|
||||||
|
<span className="font-black text-[10px] uppercase tracking-widest">دانشنامه علمی کانینا</span>
|
||||||
|
</div>
|
||||||
|
<h5 className="font-black mb-1">{termData.term}</h5>
|
||||||
|
<p className="text-white/70 text-xs leading-relaxed mb-3">
|
||||||
|
{termData.definition}
|
||||||
|
</p>
|
||||||
|
{termData.wikiId && (
|
||||||
|
<button
|
||||||
|
onClick={() => onWikiNavigate?.(termData.wikiId!)}
|
||||||
|
className="flex items-center gap-1.5 text-canina-blue hover:text-white transition-colors text-[10px] font-bold"
|
||||||
|
>
|
||||||
|
<span>مطالعه مقاله کامل</span>
|
||||||
|
<ExternalLink className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{/* Arrow */}
|
||||||
|
<div className="absolute top-full left-1/2 -translate-x-1/2 border-8 border-transparent border-t-medical-gray-900" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
153
src/components/TopUpModal.tsx
Normal file
153
src/components/TopUpModal.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { X, CreditCard, Sparkles, CheckCircle2, TrendingUp, DollarSign } from "lucide-react";
|
||||||
|
import { toPersian, cn } from "../lib/utils";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface TopUpModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: (amount: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRESET_AMOUNTS = [
|
||||||
|
{ label: "۵۰۰,۰۰۰ تومان", value: 500000 },
|
||||||
|
{ label: "۱,۰۰۰,۰۰۰ تومان", value: 1000000 },
|
||||||
|
{ label: "۲,۰۰۰,۰۰۰ تومان", value: 2000000 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function TopUpModal({ isOpen, onClose, onConfirm }: TopUpModalProps) {
|
||||||
|
const [amount, setAmount] = useState<string>("");
|
||||||
|
const [selectedPreset, setSelectedPreset] = useState<number | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handlePresetSelect = (val: number) => {
|
||||||
|
setSelectedPreset(val);
|
||||||
|
setAmount(val.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const val = e.target.value.replace(/,/g, '');
|
||||||
|
if (/^\d*$/.test(val)) {
|
||||||
|
setAmount(val);
|
||||||
|
setSelectedPreset(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const finalAmount = parseInt(amount);
|
||||||
|
if (isNaN(finalAmount) || finalAmount < 10000) {
|
||||||
|
toast.error("حداقل مبلغ شارژ ۱۰,۰۰۰ تومان است");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
// Simulate gateway connection
|
||||||
|
setTimeout(() => {
|
||||||
|
onConfirm(finalAmount);
|
||||||
|
toast.success(`مبلغ ${toPersian(finalAmount.toLocaleString())} تومان به کیف پول شما اضافه شد.`);
|
||||||
|
setLoading(false);
|
||||||
|
onClose();
|
||||||
|
setAmount("");
|
||||||
|
setSelectedPreset(null);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute inset-0 bg-medical-gray-900/60 backdrop-blur-md"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
|
className="bg-white w-full max-w-md rounded-[3rem] relative z-10 shadow-2xl overflow-hidden font-vazir text-right"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<div className="bg-medical-gray-50 px-8 py-6 border-b border-medical-gray-100 flex items-center justify-between">
|
||||||
|
<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">
|
||||||
|
<TrendingUp className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-black text-medical-gray-900 italic">شارژ سریع کیف پول</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-white border border-medical-gray-200 text-medical-gray-400 hover:text-red-500 transition-all"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="p-8 space-y-8">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">یکی از مبالغ پیشنهادی را انتخاب کنید</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{PRESET_AMOUNTS.map((preset) => (
|
||||||
|
<button
|
||||||
|
key={preset.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePresetSelect(preset.value)}
|
||||||
|
className={cn(
|
||||||
|
"px-6 py-3 rounded-2xl text-sm font-black transition-all border",
|
||||||
|
selectedPreset === preset.value
|
||||||
|
? "bg-canina-blue text-white border-canina-blue shadow-lg shadow-canina-blue/20"
|
||||||
|
: "bg-white text-medical-gray-700 border-medical-gray-100 hover:border-canina-blue"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{preset.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">یا مبلغ دلخواه خود را وارد کنید (تومان)</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={amount ? parseInt(amount).toLocaleString() : ""}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full bg-medical-gray-50 border border-medical-gray-100 rounded-3xl py-6 px-8 outline-none font-black text-2xl text-center text-medical-gray-900 transition-all focus:ring-4 focus:ring-canina-blue/10 focus:border-canina-blue italic"
|
||||||
|
placeholder="۰"
|
||||||
|
/>
|
||||||
|
{amount && (
|
||||||
|
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-xs font-bold text-medical-gray-400">تومان</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-4 flex gap-3">
|
||||||
|
<Sparkles className="w-5 h-5 text-canina-blue shrink-0" />
|
||||||
|
<p className="text-[10px] font-bold text-canina-blue leading-relaxed">با شارژ کیف پول، از هدیههای هوشمند کانینا و اولویت در ارسال سفارشها بهرهمند شوید.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading || !amount}
|
||||||
|
className="w-full py-5 bg-medical-gray-900 text-white rounded-[2rem] font-black text-lg flex items-center justify-center gap-3 hover:bg-canina-blue transition-all shadow-xl shadow-black/10 disabled:opacity-50 disabled:cursor-not-allowed group"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="w-6 h-6 border-4 border-white/20 border-t-white rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CreditCard className="w-6 h-6 group-hover:scale-110 transition-transform" />
|
||||||
|
اتصال به درگاه بانکی
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
||||||
492
src/components/UserDashboard.tsx
Normal file
492
src/components/UserDashboard.tsx
Normal file
@ -0,0 +1,492 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { User, ShoppingBag, Wallet, MapPin, LogOut, ChevronRight, Package, Calendar, UserCircle, ShoppingCart, Trash2, Edit2, Phone, Hash, CheckCircle2, ArrowUpCircle, ArrowDownCircle, Info, Clock, CheckCircle, Heart, Sparkles } from "lucide-react";
|
||||||
|
import { OrderRowSkeleton } from "./Skeleton";
|
||||||
|
import { useUserStore, Address } from "../store/userStore";
|
||||||
|
import { useCartStore } from "../store/cartStore";
|
||||||
|
import { toPersian, cn } from "../lib/utils";
|
||||||
|
import OrderDetailsModal from "./OrderDetailsModal";
|
||||||
|
import AddressModal from "./AddressModal";
|
||||||
|
import DeleteConfirmModal from "./DeleteConfirmModal";
|
||||||
|
import TopUpModal from "./TopUpModal";
|
||||||
|
|
||||||
|
export default function UserDashboard({ onBack, onNavigate }: { onBack: () => void, onNavigate: (v: any) => void }) {
|
||||||
|
const { profile, logout, addAddress, updateAddress, deleteAddress, setDefaultAddress, topUpWallet } = useUserStore();
|
||||||
|
const { orders } = useCartStore();
|
||||||
|
const [activeTab, setActiveTab] = React.useState<"profile" | "orders" | "wallet" | "addresses">("profile");
|
||||||
|
const [isLoadingOrders, setIsLoadingOrders] = useState(true);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (activeTab === "orders") {
|
||||||
|
setIsLoadingOrders(true);
|
||||||
|
const timer = setTimeout(() => setIsLoadingOrders(false), 800);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [activeTab]);
|
||||||
|
const [selectedOrder, setSelectedOrder] = useState<any>(null);
|
||||||
|
|
||||||
|
// Address State
|
||||||
|
const [isAddressModalOpen, setIsAddressModalOpen] = useState(false);
|
||||||
|
const [editingAddress, setEditingAddress] = useState<Address | null>(null);
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [addressToDelete, setAddressToDelete] = useState<Address | null>(null);
|
||||||
|
|
||||||
|
// Wallet State
|
||||||
|
const [isTopUpModalOpen, setIsTopUpModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
onBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveAddress = (addr: Address) => {
|
||||||
|
if (editingAddress) {
|
||||||
|
updateAddress(addr.id, addr);
|
||||||
|
} else {
|
||||||
|
addAddress(addr);
|
||||||
|
}
|
||||||
|
setEditingAddress(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClick = (addr: Address) => {
|
||||||
|
setAddressToDelete(addr);
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmDelete = () => {
|
||||||
|
if (addressToDelete) {
|
||||||
|
deleteAddress(addressToDelete.id);
|
||||||
|
setAddressToDelete(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditClick = (addr: Address) => {
|
||||||
|
setEditingAddress(addr);
|
||||||
|
setIsAddressModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddClick = () => {
|
||||||
|
setEditingAddress(null);
|
||||||
|
setIsAddressModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: "profile", label: "اطلاعات فردی", icon: <UserCircle className="w-5 h-5" /> },
|
||||||
|
{ id: "orders", label: "سفارشات من", icon: <Package className="w-5 h-5" /> },
|
||||||
|
{ id: "wallet", label: "کیف پول", icon: <Wallet className="w-5 h-5" /> },
|
||||||
|
{ id: "addresses", label: "آدرسها", icon: <MapPin className="w-5 h-5" /> },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-medical-gray-50 pt-12 pb-24 px-4 font-vazir" dir="rtl">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<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={onBack}>خانه</span>
|
||||||
|
<ChevronRight className="w-2.5 h-2.5" />
|
||||||
|
<span className="text-canina-blue">پنل کاربری</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid lg:grid-cols-12 gap-8">
|
||||||
|
{/* Sidebar */}
|
||||||
|
<div className="lg:col-span-4 space-y-6">
|
||||||
|
<div className="bg-white rounded-[3rem] border border-medical-gray-200 p-8 shadow-xl">
|
||||||
|
<div className="flex items-center gap-4 mb-10">
|
||||||
|
<div className="w-16 h-16 bg-canina-blue rounded-3xl flex items-center justify-center text-white shadow-lg">
|
||||||
|
<User className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-black text-medical-gray-900">{profile.firstName} {profile.lastName}</h2>
|
||||||
|
<p className="text-sm font-bold text-medical-gray-400">{profile.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{tabs.map(tab => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id as any)}
|
||||||
|
className={`w-full flex items-center gap-3 p-4 rounded-2xl transition-all font-bold ${activeTab === tab.id ? "bg-canina-blue text-white shadow-lg shadow-canina-blue/20" : "text-medical-gray-600 hover:bg-medical-gray-50"}`}
|
||||||
|
>
|
||||||
|
{tab.icon}
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="w-full flex items-center gap-3 p-4 rounded-2xl text-red-500 hover:bg-red-50 transition-all font-bold mt-4"
|
||||||
|
>
|
||||||
|
<LogOut className="w-5 h-5" />
|
||||||
|
خروج از حساب
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-medical-gray-900 rounded-[3rem] p-8 text-white relative overflow-hidden shadow-2xl">
|
||||||
|
<div className="absolute top-0 left-0 p-6 opacity-20">
|
||||||
|
<Wallet className="w-12 h-12 text-canina-blue" />
|
||||||
|
</div>
|
||||||
|
<div className="relative z-10">
|
||||||
|
<div className="text-[10px] font-black uppercase tracking-widest text-white mb-2">موجودی کیف پول</div>
|
||||||
|
<div className="flex items-baseline gap-2 flex-row-reverse justify-end">
|
||||||
|
<div className="text-4xl font-black italic">{toPersian(profile.walletBalance.toLocaleString())}</div>
|
||||||
|
<span className="text-sm not-italic font-bold text-white/80">تومان</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => { setActiveTab("wallet"); setIsTopUpModalOpen(true); }}
|
||||||
|
disabled={isTopUpModalOpen}
|
||||||
|
className="mt-8 bg-canina-blue w-full py-4 rounded-2xl font-black hover:bg-white hover:text-canina-blue transition-all shadow-lg shadow-black/20 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
شارژ کیف پول
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-pink-500 rounded-[3rem] p-8 text-white relative overflow-hidden shadow-2xl shadow-pink-200">
|
||||||
|
<div className="absolute -top-4 -right-4 opacity-10">
|
||||||
|
<Heart className="w-40 h-40 fill-white" />
|
||||||
|
</div>
|
||||||
|
<div className="relative z-10">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<Heart className="w-5 h-5 fill-white" />
|
||||||
|
<h4 className="text-sm font-black italic tracking-widest">ردپای مهربانی</h4>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-bold text-white/70 mb-1">مجموع کمکهای اهدایی</p>
|
||||||
|
<div className="flex items-baseline gap-2 flex-row-reverse justify-end">
|
||||||
|
<span className="text-3xl font-black italic">{toPersian(profile.charityDonationTotal?.toLocaleString() || "0")}</span>
|
||||||
|
<span className="text-xs font-bold opacity-60">تومان</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 bg-white/10 rounded-2xl border border-white/20 backdrop-blur-sm">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-8 h-8 bg-white/20 rounded-xl flex items-center justify-center">
|
||||||
|
<Sparkles className="w-4 h-4 text-white" />
|
||||||
|
</div>
|
||||||
|
<p className="text-[10px] font-black leading-tight">
|
||||||
|
همکاری شما باعث تامین هزینه <span className="text-sm text-yellow-300 mx-1">{toPersian(Math.floor((profile.charityDonationTotal || 0) / 15000).toString())} وعده غذا</span> برای حیوانات بیپناه شده است.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="lg:col-span-8">
|
||||||
|
<motion.div
|
||||||
|
key={activeTab}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-white rounded-[3.5rem] border border-medical-gray-200 p-10 min-h-[600px] shadow-sm overflow-hidden"
|
||||||
|
>
|
||||||
|
{activeTab === "profile" && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-black text-medical-gray-900 mb-8 italic">اطلاعات فردی</h3>
|
||||||
|
<div className="grid md:grid-cols-2 gap-8">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-4">نام</label>
|
||||||
|
<input type="text" readOnly value={profile.firstName} className="w-full bg-medical-gray-50 border border-medical-gray-100 p-4 rounded-2xl font-bold outline-none" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-4">نام خانوادگی</label>
|
||||||
|
<input type="text" readOnly value={profile.lastName} className="w-full bg-medical-gray-50 border border-medical-gray-100 p-4 rounded-2xl font-bold outline-none" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 md:col-span-2">
|
||||||
|
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-4">ایمیل</label>
|
||||||
|
<input type="text" readOnly value={profile.email} className="w-full bg-medical-gray-50 border border-medical-gray-100 p-4 rounded-2xl font-bold outline-none" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="mt-12 bg-medical-gray-900 text-white px-10 py-4 rounded-2xl font-black hover:bg-canina-blue transition-all">
|
||||||
|
ویرایش اطلاعات
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "orders" && (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<h3 className="text-2xl font-black text-medical-gray-900 italic">تاریخچه سفارشات</h3>
|
||||||
|
<div className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest border border-medical-gray-100 px-4 py-2 rounded-full bg-medical-gray-50">
|
||||||
|
تعداد کل: {toPersian((orders || []).length.toString())} مورد
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{(orders || []).length === 0 ? (
|
||||||
|
<div className="text-center py-20">
|
||||||
|
<ShoppingBag className="w-16 h-16 text-medical-gray-200 mx-auto mb-4" />
|
||||||
|
<p className="text-medical-gray-400 font-bold">هنوز سفارشی ثبت نکردهاید.</p>
|
||||||
|
<button onClick={() => onNavigate("shop")} className="mt-6 text-canina-blue font-black underline underline-offset-8">برو به فروشگاه</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{isLoadingOrders ? (
|
||||||
|
[1, 2, 3].map(i => <OrderRowSkeleton key={i} />)
|
||||||
|
) : (
|
||||||
|
(orders || []).map(order => (
|
||||||
|
<motion.div
|
||||||
|
key={order.id}
|
||||||
|
whileHover={{ scale: 1.01, x: -5 }}
|
||||||
|
onClick={() => setSelectedOrder(order)}
|
||||||
|
className="p-6 border border-medical-gray-100 rounded-[2.5rem] hover:border-canina-blue transition-all group cursor-pointer bg-white overflow-hidden relative"
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 bg-medical-gray-50 rounded-2xl flex items-center justify-center text-canina-blue group-hover:bg-canina-blue group-hover:text-white transition-all shadow-sm">
|
||||||
|
<Package className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors italic">سفارش {toPersian(order.id.toUpperCase())}</div>
|
||||||
|
<div className="text-[10px] font-bold text-medical-gray-400 flex items-center gap-1">
|
||||||
|
<Calendar className="w-3 h-3 opacity-30" />
|
||||||
|
{toPersian(new Date(order.date).toLocaleDateString("fa-IR"))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="text-lg font-black text-medical-gray-900 italic">{toPersian(order.total.toLocaleString())} <span className="text-xs">تومان</span></div>
|
||||||
|
<div className="flex items-center gap-2 justify-end mt-1">
|
||||||
|
<span className={`text-[9px] font-black px-2 py-0.5 rounded-full uppercase tracking-tighter ${order.status === 'delivered' ? 'bg-green-50 text-green-600' : 'bg-canina-blue/5 text-canina-blue'}`}>
|
||||||
|
{order.status === 'delivered' ? 'تحویل شده' : 'ارسال شده'}
|
||||||
|
</span>
|
||||||
|
<ChevronRight className="w-4 h-4 text-medical-gray-300 -rotate-180" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "addresses" && (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<h3 className="text-2xl font-black text-medical-gray-900 italic">آدرسهای من</h3>
|
||||||
|
<button
|
||||||
|
onClick={handleAddClick}
|
||||||
|
className="px-6 py-3 bg-canina-blue text-white rounded-2xl font-black text-xs flex items-center gap-2 hover:bg-medical-gray-900 transition-all shadow-lg shadow-canina-blue/10"
|
||||||
|
>
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
افزودن آدرس جدید
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{(profile?.addresses || []).length === 0 ? (
|
||||||
|
<div className="text-center py-20 border-2 border-dashed border-medical-gray-100 rounded-[3rem]">
|
||||||
|
<MapPin className="w-16 h-16 text-medical-gray-200 mx-auto mb-4" />
|
||||||
|
<p className="text-medical-gray-400 font-bold">هنوز آدرسی ثبت نکردهاید.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(profile?.addresses || []).map((addr) => (
|
||||||
|
<motion.div
|
||||||
|
key={addr.id}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className={cn(
|
||||||
|
"p-8 bg-white border rounded-[2.5rem] relative group transition-all",
|
||||||
|
addr.isDefault ? "border-canina-blue shadow-xl shadow-canina-blue/5" : "border-medical-gray-100 hover:border-medical-gray-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start mb-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className={cn("w-12 h-12 rounded-2xl flex items-center justify-center transition-all", addr.isDefault ? "bg-canina-blue text-white shadow-lg" : "bg-medical-gray-50 text-canina-blue")}>
|
||||||
|
<MapPin className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<h4 className="text-xl font-black text-medical-gray-900 italic">{addr.title}</h4>
|
||||||
|
{addr.isDefault && (
|
||||||
|
<span className="px-3 py-1 bg-green-50 text-green-600 text-[9px] font-black rounded-full uppercase tracking-widest">پیشفرض</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs font-bold text-medical-gray-400">تحویل گیرنده: {addr.receptorName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEditClick(addr)}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-medical-gray-50 text-medical-gray-400 hover:bg-canina-blue hover:text-white transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
<Edit2 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteClick(addr)}
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-medical-gray-50 text-medical-gray-400 hover:bg-red-500 hover:text-white transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-medical-gray-50/50 rounded-2xl p-6 mb-6">
|
||||||
|
<p className="text-sm font-black text-medical-gray-700 leading-relaxed text-right">{toPersian(addr.province)}، {toPersian(addr.city)}، {toPersian(addr.detail)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Phone className="w-4 h-4 text-canina-blue/40" />
|
||||||
|
<span className="text-xs font-bold text-medical-gray-500" dir="ltr">{toPersian(addr.phone)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Hash className="w-4 h-4 text-canina-blue/40" />
|
||||||
|
<span className="text-xs font-bold text-medical-gray-500" dir="ltr">{toPersian(addr.zipCode)}</span>
|
||||||
|
</div>
|
||||||
|
{!addr.isDefault && (
|
||||||
|
<button
|
||||||
|
onClick={() => setDefaultAddress(addr.id)}
|
||||||
|
className="mr-auto text-[10px] font-black text-canina-blue hover:text-medical-gray-900 uppercase tracking-widest whitespace-nowrap"
|
||||||
|
>
|
||||||
|
انتخاب به عنوان پیشفرض
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{addr.isDefault && (
|
||||||
|
<div className="mr-auto flex items-center gap-2 text-green-600 text-[10px] font-black italic">
|
||||||
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
|
آدرس اصلی ارسال
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "wallet" && (
|
||||||
|
<div className="space-y-10">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-2xl font-black text-medical-gray-900 italic">مدیریت کیف پول</h3>
|
||||||
|
<div className="text-[10px] font-black text-green-600 bg-green-50 px-4 py-2 rounded-full uppercase tracking-widest border border-green-100 flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-3 h-3" />
|
||||||
|
حساب تایید شده
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Balance Card */}
|
||||||
|
<div className="bg-gradient-to-br from-canina-blue to-blue-600 rounded-[3rem] p-12 text-white shadow-2xl shadow-canina-blue/20 relative overflow-hidden group">
|
||||||
|
<div className="absolute top-0 right-0 p-12 opacity-10 group-hover:scale-110 transition-transform duration-700">
|
||||||
|
<Wallet className="w-48 h-48" />
|
||||||
|
</div>
|
||||||
|
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-10">
|
||||||
|
<div className="text-center md:text-right">
|
||||||
|
<div className="text-xs font-black uppercase tracking-[0.2em] text-white/70 mb-4 flex items-center gap-2 justify-center md:justify-start text-right">
|
||||||
|
<div className="w-2 h-2 bg-white rounded-full animate-pulse" />
|
||||||
|
موجودی زنده و قابل استفاده
|
||||||
|
</div>
|
||||||
|
<div className="flex items-baseline gap-4 flex-row-reverse justify-center md:justify-start">
|
||||||
|
<div className="text-6xl font-black italic tracking-tight">{toPersian(profile.walletBalance.toLocaleString())}</div>
|
||||||
|
<span className="text-xl not-italic font-bold opacity-80 decoration-white/30 underline underline-offset-8">تومان</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 w-full md:w-auto">
|
||||||
|
<div className="relative group/btn flex-1 md:flex-none">
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
className="w-full md:w-40 h-16 bg-white/10 backdrop-blur-md rounded-2xl font-black text-sm flex items-center justify-center gap-2 border border-white/20 opacity-50 cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<ArrowDownCircle className="w-5 h-5" />
|
||||||
|
برداشت وجه
|
||||||
|
</button>
|
||||||
|
<div className="absolute top-full right-0 mt-3 w-48 p-3 bg-medical-gray-900 text-white text-[10px] font-bold rounded-xl opacity-0 group-hover/btn:opacity-100 transition-all pointer-events-none z-20 text-center shadow-xl">
|
||||||
|
قابلیت برداشت وجه بهزودی فعال خواهد شد.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsTopUpModalOpen(true)}
|
||||||
|
className="flex-1 md:w-40 h-16 bg-white text-canina-blue rounded-2xl font-black text-sm flex items-center justify-center gap-2 hover:bg-medical-gray-900 hover:text-white transition-all shadow-xl shadow-black/10 group/topup"
|
||||||
|
>
|
||||||
|
<ArrowUpCircle className="w-5 h-5 group-hover:-translate-y-1 transition-transform" />
|
||||||
|
شارژ آنی
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Transaction History */}
|
||||||
|
<div className="pt-10 border-t border-medical-gray-100">
|
||||||
|
<div className="flex items-center gap-4 mb-8">
|
||||||
|
<div className="w-10 h-10 bg-medical-gray-50 rounded-xl flex items-center justify-center text-medical-gray-400">
|
||||||
|
<Clock className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<h4 className="text-lg font-black text-medical-gray-900 italic">تاریخچه تراکنشها</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{(profile?.transactions || []).length === 0 ? (
|
||||||
|
<div className="text-center py-20 bg-medical-gray-50 rounded-[2.5rem] border border-dashed border-medical-gray-200">
|
||||||
|
<Info className="w-12 h-12 text-medical-gray-200 mx-auto mb-4" />
|
||||||
|
<p className="text-medical-gray-400 font-bold">تراکنشی یافت نشد.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(profile?.transactions || []).map((trx) => (
|
||||||
|
<div key={trx.id} className="p-6 bg-white border border-medical-gray-100 rounded-[2rem] flex flex-wrap items-center justify-between gap-6 hover:shadow-lg hover:border-canina-blue/10 transition-all group">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className={cn(
|
||||||
|
"w-12 h-12 rounded-2xl flex items-center justify-center transition-colors",
|
||||||
|
trx.type === 'top_up' ? "bg-green-50 text-green-500 group-hover:bg-green-500 group-hover:text-white" : "bg-red-50 text-red-500 group-hover:bg-red-500 group-hover:text-white"
|
||||||
|
)}>
|
||||||
|
{trx.type === 'top_up' ? <ArrowUpCircle className="w-6 h-6" /> : <ShoppingCart className="w-6 h-6" />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-black text-medical-gray-900">{trx.type === 'top_up' ? "افزایش موجودی (شارژ)" : "پرداخت سفارش"}</div>
|
||||||
|
<div className="text-[10px] font-bold text-medical-gray-400 flex items-center gap-2">
|
||||||
|
<span>کد پیگیری: {trx.id}</span>
|
||||||
|
<span className="w-1 h-1 bg-medical-gray-200 rounded-full" />
|
||||||
|
<span>{toPersian(new Date(trx.date).toLocaleDateString("fa-IR"))}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className={cn(
|
||||||
|
"text-lg font-black italic",
|
||||||
|
trx.type === 'top_up' ? "text-green-600" : "text-red-500"
|
||||||
|
)}>
|
||||||
|
{trx.type === 'top_up' ? "+" : ""}{toPersian(trx.amount.toLocaleString())} <span className="text-xs not-italic">تومان</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[9px] font-black uppercase text-green-500 tracking-widest mt-1">
|
||||||
|
تایید شده
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<OrderDetailsModal
|
||||||
|
isOpen={!!selectedOrder}
|
||||||
|
onClose={() => setSelectedOrder(null)}
|
||||||
|
order={selectedOrder}
|
||||||
|
/>
|
||||||
|
<AddressModal
|
||||||
|
isOpen={isAddressModalOpen}
|
||||||
|
onClose={() => setIsAddressModalOpen(false)}
|
||||||
|
onSave={handleSaveAddress}
|
||||||
|
editingAddress={editingAddress}
|
||||||
|
/>
|
||||||
|
<DeleteConfirmModal
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
onClose={() => setIsDeleteModalOpen(false)}
|
||||||
|
onConfirm={handleConfirmDelete}
|
||||||
|
title="حذف آدرس"
|
||||||
|
message={`آیا از حذف آدرس "${addressToDelete?.title}" اطمینان دارید؟ این عمل غیرقابل بازگشت است.`}
|
||||||
|
/>
|
||||||
|
<TopUpModal
|
||||||
|
isOpen={isTopUpModalOpen}
|
||||||
|
onClose={() => setIsTopUpModalOpen(false)}
|
||||||
|
onConfirm={topUpWallet}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
147
src/components/VetGallery.tsx
Normal file
147
src/components/VetGallery.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Play, PlayCircle, Star, ShieldCheck, X } from "lucide-react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
|
||||||
|
const VIDEOS = [
|
||||||
|
{
|
||||||
|
id: "v1",
|
||||||
|
title: "نحوه آمادهسازی کانیهیدروکس",
|
||||||
|
doctor: "دکتر کلاوس هنینگ",
|
||||||
|
duration: "۱:۴۵",
|
||||||
|
thumbnail: "https://images.unsplash.com/photo-1576091160550-217359f42f8c?auto=format&fit=crop&q=80&w=400",
|
||||||
|
videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4", // Placeholder real-looking video
|
||||||
|
description: "آموزش گام به گام آمادهسازی پودر Canhydrox GAG برای جذب حداکثری در دستگاه گوارش."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "v2",
|
||||||
|
title: "اهمیت صدف لبسبز",
|
||||||
|
doctor: "دکتر النا اشمیت",
|
||||||
|
duration: "۲:۲۰",
|
||||||
|
thumbnail: "https://images.unsplash.com/photo-1599443015574-be5fe8a05783?auto=format&fit=crop&q=80&w=400",
|
||||||
|
videoUrl: "https://www.w3schools.com/html/movie.mp4",
|
||||||
|
description: "چرا صدف لبسبز نیوزیلندی نایابترین و موثرترین ماده برای بازسازی مفصل است؟"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "v3",
|
||||||
|
title: "تغذیه هوشمند تولهسگها",
|
||||||
|
doctor: "دکتر هلگا فیشر",
|
||||||
|
duration: "۳:۱۰",
|
||||||
|
thumbnail: "https://images.unsplash.com/photo-1583337130417-3346a1be7dee?auto=format&fit=crop&q=80&w=400",
|
||||||
|
videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
|
||||||
|
description: "دوز مصرفی مکملهای کلسیم در نژادهای بزرگ برای جلوگیری از بدشکلیهای استخوانی."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function VetGallery({ onNavigate }: { onNavigate?: (v: any) => void }) {
|
||||||
|
const [selectedVideo, setSelectedVideo] = useState<typeof VIDEOS[0] | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-white font-vazir" dir="rtl">
|
||||||
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-end justify-between mb-16 gap-6">
|
||||||
|
<div className="max-w-2xl">
|
||||||
|
<div className="inline-flex items-center gap-2 px-3 py-1 bg-canina-blue/10 text-canina-blue rounded-full text-[10px] font-black uppercase tracking-widest mb-4">
|
||||||
|
<Star className="w-3 h-3" />
|
||||||
|
Vet Insights
|
||||||
|
</div>
|
||||||
|
<h2 className="text-4xl md:text-5xl font-black text-medical-gray-900 italic leading-tight">
|
||||||
|
مشاوره ویدئویی <span className="text-canina-blue underline decoration-4 decoration-canina-blue/20 underline-offset-8">دوست دامپزشک پت شما</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-medical-gray-500 font-medium mt-6">
|
||||||
|
متخصصین آلمانی کانینا مستقیماً با شما صحبت میکنند. نحوه مصرف، مزایای رقابتی و دانش تخصصی پشت هر محصول.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => onNavigate?.('videos')}
|
||||||
|
className="flex items-center gap-2 text-canina-blue font-black border-b-2 border-canina-blue pb-1 hover:gap-4 transition-all whitespace-nowrap"
|
||||||
|
>
|
||||||
|
مشاهده همه ویدئوها
|
||||||
|
<Play className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{VIDEOS.map((video, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={video.id}
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className="group cursor-pointer"
|
||||||
|
onClick={() => setSelectedVideo(video)}
|
||||||
|
>
|
||||||
|
<div className="relative aspect-video rounded-[2rem] overflow-hidden mb-6 shadow-xl">
|
||||||
|
<img
|
||||||
|
src={video.thumbnail}
|
||||||
|
alt={video.title}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/30 group-hover:bg-black/20 transition-colors flex items-center justify-center">
|
||||||
|
<div className="w-16 h-16 bg-white/90 rounded-full flex items-center justify-center text-canina-blue shadow-2xl group-hover:scale-110 transition-transform">
|
||||||
|
<PlayCircle className="w-10 h-10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-4 right-4 bg-black/60 backdrop-blur-md text-white px-3 py-1 rounded-lg text-[10px] font-black">
|
||||||
|
{video.duration}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<ShieldCheck className="w-4 h-4 text-canina-blue" />
|
||||||
|
<span className="text-xs font-black text-canina-blue">{video.doctor}</span>
|
||||||
|
</div>
|
||||||
|
<h4 className="text-xl font-black text-medical-gray-900 group-hover:text-canina-blue transition-colors mb-3">
|
||||||
|
{video.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-medical-gray-500 font-medium leading-relaxed line-clamp-2 italic">
|
||||||
|
"{video.description}"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Video Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedVideo && (
|
||||||
|
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={() => setSelectedVideo(null)}
|
||||||
|
className="absolute inset-0 bg-medical-gray-900/90 backdrop-blur-xl"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9 }}
|
||||||
|
className="bg-black w-full max-w-5xl aspect-video rounded-[3rem] overflow-hidden relative z-10 shadow-2xl border border-white/10"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedVideo(null)}
|
||||||
|
className="absolute top-6 left-6 text-white/50 hover:text-white z-20 transition-colors bg-black/20 p-2 rounded-full"
|
||||||
|
>
|
||||||
|
<X className="w-8 h-8" />
|
||||||
|
</button>
|
||||||
|
<video
|
||||||
|
src={selectedVideo.videoUrl}
|
||||||
|
controls
|
||||||
|
autoPlay
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-8 bg-gradient-to-t from-black/80 to-transparent">
|
||||||
|
<h3 className="text-white text-2xl font-black mb-2">{selectedVideo.title}</h3>
|
||||||
|
<p className="text-white/60 text-sm font-medium">{selectedVideo.doctor}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
185
src/components/VideosPage.tsx
Normal file
185
src/components/VideosPage.tsx
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { ChevronRight, PlayCircle, Star, ShieldCheck, X, Search, Clock, User } from "lucide-react";
|
||||||
|
import { Product, PRODUCTS } from "../data/products";
|
||||||
|
|
||||||
|
const ALL_VIDEOS = [
|
||||||
|
{
|
||||||
|
id: "v1",
|
||||||
|
title: "نحوه آمادهسازی کانیهیدروکس",
|
||||||
|
doctor: "دکتر کلاوس هنینگ",
|
||||||
|
duration: "۱:۴۵",
|
||||||
|
thumbnail: "https://images.unsplash.com/photo-1576091160550-217359f42f8c?auto=format&fit=crop&q=80&w=600",
|
||||||
|
videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
|
||||||
|
description: "آموزش گام به گام آمادهسازی پودر Canhydrox GAG برای جذب حداکثری در دستگاه گوارش."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "v2",
|
||||||
|
title: "اهمیت صدف لبسبز برای غضروف",
|
||||||
|
doctor: "دکتر النا اشمیت",
|
||||||
|
duration: "۲:۲۰",
|
||||||
|
thumbnail: "https://images.unsplash.com/photo-1599443015574-be5fe8a05783?auto=format&fit=crop&q=80&w=600",
|
||||||
|
videoUrl: "https://www.w3schools.com/html/movie.mp4",
|
||||||
|
description: "چرا صدف لبسبز نیوزیلندی نایابترین و موثرترین ماده برای بازسازی مفصل است؟"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "v3",
|
||||||
|
title: "تغذیه هوشمند تولهسگهای نژاد بزرگ",
|
||||||
|
doctor: "دکتر هلگا فیشر",
|
||||||
|
duration: "۳:۱۰",
|
||||||
|
thumbnail: "https://images.unsplash.com/photo-1583337130417-3346a1be7dee?auto=format&fit=crop&q=80&w=600",
|
||||||
|
videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
|
||||||
|
description: "دوز مصرفی مکملهای کلسیم در نژادهای بزرگ برای جلوگیری از بدشکلیهای استخوانی."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "v4",
|
||||||
|
title: "تاثیر امگا ۳ بر ریزش مو فصلی",
|
||||||
|
doctor: "دکتر ماری کونیگ",
|
||||||
|
duration: "۵:۱۵",
|
||||||
|
thumbnail: "https://images.unsplash.com/photo-1516734212186-a967f81ad0d7?auto=format&fit=crop&q=80&w=600",
|
||||||
|
videoUrl: "https://www.w3schools.com/html/movie.mp4",
|
||||||
|
description: "چربیهای ضروری چگونه لایه محافظ پوست را بازسازی میکنند."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "v5",
|
||||||
|
title: "سمزدایی گوارشی با اسید هومیک",
|
||||||
|
doctor: "دکتر ریکاردو کوخ",
|
||||||
|
duration: "۴:۰۰",
|
||||||
|
thumbnail: "https://images.unsplash.com/photo-1581594693702-fbdc51b2763b?auto=format&fit=crop&q=80&w=600",
|
||||||
|
videoUrl: "https://www.w3schools.com/html/mov_bbb.mp4",
|
||||||
|
description: "مکانیزم عمل Moor-Tranke در جذب توکسینهای رودهای."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function VideosPage({ onBack }: { onBack: () => void }) {
|
||||||
|
const [selectedVideo, setSelectedVideo] = useState<typeof ALL_VIDEOS[0] | null>(null);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const filteredVideos = ALL_VIDEOS.filter(v =>
|
||||||
|
v.title.includes(search) || v.doctor.includes(search)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-medical-gray-900 pt-8 pb-20 px-4 font-vazir" dir="rtl">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col md:flex-row items-center justify-between mb-16 gap-6">
|
||||||
|
<div className="text-right">
|
||||||
|
<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">
|
||||||
|
<PlayCircle className="w-3 h-3" />
|
||||||
|
Video Academy
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl lg:text-6xl font-black text-white italic leading-tight">
|
||||||
|
آکادمی ویدئویی <span className="text-canina-blue">کانینا</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="px-8 py-3 bg-white/5 border border-white/10 rounded-2xl text-white shadow-sm flex items-center gap-2 font-black hover:bg-white/10 transition-all"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
بازگشت
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="mb-12 relative max-w-2xl">
|
||||||
|
<Search className="absolute right-6 top-1/2 -translate-y-1/2 text-white/30 w-5 h-5" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="جستجو در ویدئوهای آموزشی..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
className="w-full bg-white/5 border border-white/10 rounded-[2rem] py-5 pr-14 pl-6 text-white outline-none focus:bg-white/10 transition-all font-bold"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Video Grid */}
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{filteredVideos.map((video, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={video.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className="group cursor-pointer"
|
||||||
|
onClick={() => setSelectedVideo(video)}
|
||||||
|
>
|
||||||
|
<div className="relative aspect-video rounded-[2.5rem] overflow-hidden mb-6 shadow-2xl border border-white/5">
|
||||||
|
<img
|
||||||
|
src={video.thumbnail}
|
||||||
|
alt={video.title}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700 opacity-60 group-hover:opacity-100"
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="w-16 h-16 bg-canina-blue rounded-full flex items-center justify-center text-white shadow-2xl group-hover:scale-110 transition-transform">
|
||||||
|
<PlayCircle className="w-10 h-10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-4 right-4 bg-black/60 backdrop-blur-md text-white px-3 py-1 rounded-lg text-[10px] font-black">
|
||||||
|
{video.duration}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-4">
|
||||||
|
<h3 className="text-xl font-black text-white group-hover:text-canina-blue transition-colors mb-2">{video.title}</h3>
|
||||||
|
<div className="flex items-center gap-4 text-xs font-bold text-white/40">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<User className="w-3 h-3" />
|
||||||
|
{video.doctor}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
دسترسی دائمی
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Video Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedVideo && (
|
||||||
|
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={() => setSelectedVideo(null)}
|
||||||
|
className="absolute inset-0 bg-black/95 backdrop-blur-xl"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9 }}
|
||||||
|
className="bg-black w-full max-w-5xl aspect-video rounded-[3rem] overflow-hidden relative z-10 shadow-2xl border border-white/10"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedVideo(null)}
|
||||||
|
className="absolute top-6 left-6 text-white/50 hover:text-white z-20 transition-colors bg-white/5 p-2 rounded-full"
|
||||||
|
>
|
||||||
|
<X className="w-8 h-8" />
|
||||||
|
</button>
|
||||||
|
<video
|
||||||
|
src={selectedVideo.videoUrl}
|
||||||
|
controls
|
||||||
|
autoPlay
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-10 bg-gradient-to-t from-black/90 to-transparent">
|
||||||
|
<div className="flex items-center gap-2 text-canina-blue text-xs font-black mb-2 uppercase tracking-widest">
|
||||||
|
<ShieldCheck className="w-4 h-4" />
|
||||||
|
Verified Insight
|
||||||
|
</div>
|
||||||
|
<h3 className="text-white text-3xl font-black mb-2 italic">{selectedVideo.title}</h3>
|
||||||
|
<p className="text-white/60 text-lg font-medium">{selectedVideo.doctor}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
513
src/data/products.ts
Normal file
513
src/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
src/data/scientificTerms.ts
Normal file
43
src/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
src/hooks/useNetworkStatus.ts
Normal file
20
src/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;
|
||||||
|
}
|
||||||
74
src/index.css
Normal file
74
src/index.css
Normal file
@ -0,0 +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";
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
8
src/lib/utils.ts
Normal file
8
src/lib/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(" ");
|
||||||
|
}
|
||||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import {StrictMode} from 'react';
|
||||||
|
import {createRoot} from 'react-dom/client';
|
||||||
|
import App from './App.tsx';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
71
src/services/productService.ts
Normal file
71
src/services/productService.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulates an API call to get all products with optional filters
|
||||||
|
*/
|
||||||
|
public async getProducts(filters?: {
|
||||||
|
category?: string;
|
||||||
|
petType?: PetType | "all";
|
||||||
|
query?: string;
|
||||||
|
}): Promise<Product[]> {
|
||||||
|
// TODO: [BACKEND_API] GET /api/products | Payload: { category?: string, petType?: string, query?: string } | Expected: Product[] | Errors: [500]
|
||||||
|
// Simulate network delay
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 600));
|
||||||
|
|
||||||
|
let filtered = [...PRODUCTS];
|
||||||
|
|
||||||
|
if (filters?.category && filters.category !== "all") {
|
||||||
|
const target = filters.category.trim().toLowerCase();
|
||||||
|
console.log("[ProductService] Filtering by Category (Input):", target);
|
||||||
|
|
||||||
|
filtered = filtered.filter(p => {
|
||||||
|
const catName = p.category.trim().toLowerCase();
|
||||||
|
const catSlug = (p.categorySlug || "").trim().toLowerCase();
|
||||||
|
const match = catName === target || catSlug === target;
|
||||||
|
if (match) {
|
||||||
|
console.log(`[ProductService] Found Match: ${p.name} (Cat: ${catName}, Slug: ${catSlug})`);
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters?.petType && filters.petType !== "all") {
|
||||||
|
filtered = filtered.filter(p => p.suitableFor === filters.petType || p.suitableFor === "هر دو");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters?.query) {
|
||||||
|
const q = filters.query.toLowerCase();
|
||||||
|
filtered = filtered.filter(p =>
|
||||||
|
p.name.toLowerCase().includes(q) ||
|
||||||
|
p.description.toLowerCase().includes(q) ||
|
||||||
|
p.symptoms.some(s => s.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getProductById(id: string): Promise<Product | null> {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
return PRODUCTS.find(p => p.id === id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getFeaturedProducts(): Promise<Product[]> {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 400));
|
||||||
|
return PRODUCTS.slice(0, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const productService = ProductService.getInstance();
|
||||||
132
src/store/cartStore.ts
Normal file
132
src/store/cartStore.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
import { Product } from "../data/products";
|
||||||
|
|
||||||
|
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) => boolean;
|
||||||
|
addOrder: (order: Omit<Order, 'id' | 'date' | 'status' | 'trackingNumber'>) => string;
|
||||||
|
clearCart: () => void;
|
||||||
|
removeCoupon: () => 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: (code) => {
|
||||||
|
// TODO: [BACKEND_API] POST /api/cart/apply-coupon | Payload: { code: string } | Expected: { discount: number } | Errors: [400, 404]
|
||||||
|
const cleanCode = code.toUpperCase().trim();
|
||||||
|
if (cleanCode === 'CANINA2024') {
|
||||||
|
set({ coupon: { code: 'CANINA2024', discount: 0.1 } });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cleanCode === 'FIRST_PET') {
|
||||||
|
set({ coupon: { code: 'FIRST_PET', discount: 150000 } });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
addOrder: (orderData) => {
|
||||||
|
// TODO: [BACKEND_API] POST /api/orders | Payload: Omit<Order, 'id' | 'date' | 'status' | 'trackingNumber'> | Expected: Order | Errors: [400, 401]
|
||||||
|
const id = `CN-${Math.floor(Math.random() * 90000) + 10000}`;
|
||||||
|
const newOrder: Order = {
|
||||||
|
...orderData,
|
||||||
|
id,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
status: 'processing',
|
||||||
|
trackingNumber: `IR-${Math.floor(Math.random() * 900000) + 100000}`
|
||||||
|
};
|
||||||
|
set({ orders: [newOrder, ...get().orders] });
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
clearCart: () => set({ items: [], coupon: null }),
|
||||||
|
removeCoupon: () => set({ coupon: null }),
|
||||||
|
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" }
|
||||||
|
)
|
||||||
|
);
|
||||||
182
src/store/usePetStore.ts
Normal file
182
src/store/usePetStore.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
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">) => void;
|
||||||
|
toggleReminder: (petId: string, reminderId: string, date: string) => void;
|
||||||
|
addHealthLog: (petId: string, log: Omit<HealthLog, "id" | "date">) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePetStore = create<PetStore>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
pets: [
|
||||||
|
{
|
||||||
|
id: "lucy",
|
||||||
|
name: "لوسی",
|
||||||
|
type: "سگ",
|
||||||
|
breed: "گلدن رتریور",
|
||||||
|
age: 3,
|
||||||
|
weight: 28,
|
||||||
|
activityLevel: "زیاد",
|
||||||
|
medicalConditions: ["حساسیت فصلی"],
|
||||||
|
reminders: [],
|
||||||
|
logs: [],
|
||||||
|
consumptions: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "rocky",
|
||||||
|
name: "رکی",
|
||||||
|
type: "سگ",
|
||||||
|
breed: "ژرمن شپرد",
|
||||||
|
age: 5,
|
||||||
|
weight: 35,
|
||||||
|
activityLevel: "متوسط",
|
||||||
|
medicalConditions: ["دیسپلازی لگن"],
|
||||||
|
reminders: [],
|
||||||
|
logs: [],
|
||||||
|
consumptions: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
activePetId: "lucy",
|
||||||
|
addPet: (pet) => {
|
||||||
|
// TODO: [BACKEND_API] POST /api/pets | Payload: Omit<PetProfile, "id"> | Expected: PetProfile | Errors: [400, 401]
|
||||||
|
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: (id) => {
|
||||||
|
// TODO: [BACKEND_API] DELETE /api/pets/{id} | Expected: { success: boolean } | Errors: [401, 404]
|
||||||
|
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: (id, updates) => {
|
||||||
|
// TODO: [BACKEND_API] PATCH /api/pets/{id} | Payload: Partial<PetProfile> | Expected: PetProfile | Errors: [400, 401, 404]
|
||||||
|
set((state) => ({
|
||||||
|
pets: state.pets.map((p) => (p.id === id ? { ...p, ...updates } : p)),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
addReminder: (petId, reminder) => {
|
||||||
|
// TODO: [BACKEND_API] POST /api/pets/{petId}/reminders | Payload: Omit<Reminder, "id"> | Expected: Reminder | Errors: [400, 401, 404]
|
||||||
|
const id = Math.random().toString(36).substring(7);
|
||||||
|
set((state) => ({
|
||||||
|
pets: state.pets.map((p) =>
|
||||||
|
p.id === petId ? { ...p, reminders: [...(p.reminders || []), { ...reminder, id, completedDates: [] }] } : p
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
toggleReminder: (petId, reminderId, date) => {
|
||||||
|
// TODO: [BACKEND_API] POST /api/pets/{petId}/reminders/{reminderId}/toggle | Payload: { date: string } | Expected: { success: boolean, newRemaining?: number } | Errors: [401, 404]
|
||||||
|
set((state) => ({
|
||||||
|
pets: state.pets.map((p) => {
|
||||||
|
if (p.id !== petId) return p;
|
||||||
|
|
||||||
|
const reminder = p.reminders?.find(r => r.id === reminderId);
|
||||||
|
if (!reminder) return p;
|
||||||
|
|
||||||
|
const isCompleting = !reminder.completedDates.includes(date);
|
||||||
|
|
||||||
|
// 1. Update Reminders
|
||||||
|
const newReminders = p.reminders.map(r => {
|
||||||
|
if (r.id !== reminderId) return r;
|
||||||
|
return {
|
||||||
|
...r,
|
||||||
|
completedDates: isCompleting
|
||||||
|
? [...r.completedDates, date]
|
||||||
|
: r.completedDates.filter(d => d !== date)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Update Consumptions (decrement if completing and product is linked)
|
||||||
|
let newConsumptions = p.consumptions || [];
|
||||||
|
if (isCompleting && reminder.productId) {
|
||||||
|
newConsumptions = newConsumptions.map(c =>
|
||||||
|
c.productId === reminder.productId
|
||||||
|
? { ...c, remaining: Math.max(0, c.remaining - 1) }
|
||||||
|
: c
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...p, reminders: newReminders, consumptions: newConsumptions };
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
addHealthLog: (petId, log) => {
|
||||||
|
// TODO: [BACKEND_API] POST /api/pets/{petId}/health-logs | Payload: Omit<HealthLog, "id" | "date"> | Expected: HealthLog | Errors: [400, 401, 404]
|
||||||
|
const id = Math.random().toString(36).substring(7);
|
||||||
|
const date = new Date().toISOString();
|
||||||
|
set((state) => ({
|
||||||
|
pets: state.pets.map((p) =>
|
||||||
|
p.id === petId ? { ...p, logs: [{ ...log, id, date }, ...(p.logs || [])] } : p
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ name: "canina-pets" }
|
||||||
|
)
|
||||||
|
);
|
||||||
150
src/store/userStore.ts
Normal file
150
src/store/userStore.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
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;
|
||||||
|
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>) => void;
|
||||||
|
addAddress: (address: Address) => void;
|
||||||
|
updateAddress: (id: string, address: Address) => void;
|
||||||
|
deleteAddress: (id: string) => void;
|
||||||
|
setDefaultAddress: (id: string) => void;
|
||||||
|
topUpWallet: (amount: number) => void;
|
||||||
|
logout: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = create<UserStore>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
role: "User_Guest",
|
||||||
|
isLoggedIn: false,
|
||||||
|
profile: {
|
||||||
|
firstName: "پارسا",
|
||||||
|
lastName: "آقایی",
|
||||||
|
email: "parsa.aghaee@gmail.com",
|
||||||
|
walletBalance: 2450000,
|
||||||
|
charityDonationTotal: 45000,
|
||||||
|
addresses: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
title: "منزل اصلی",
|
||||||
|
receptorName: "پارسا آقایی",
|
||||||
|
phone: "09121234567",
|
||||||
|
province: "تهران",
|
||||||
|
city: "تهران",
|
||||||
|
detail: "سعادتآباد، خیابان سرو غربی، کوچه هفتم، پلاک ۴",
|
||||||
|
zipCode: "1998765432",
|
||||||
|
isDefault: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
transactions: [
|
||||||
|
{ id: "TRX-101", type: "top_up", amount: 1000000, date: new Date(Date.now() - 86400000 * 2).toISOString(), status: "success" },
|
||||||
|
{ id: "TRX-102", type: "purchase", amount: -550000, date: new Date(Date.now() - 86400000).toISOString(), status: "success" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
setRole: (role) => set({ role }),
|
||||||
|
setLoggedIn: (isLoggedIn) => set({ isLoggedIn }),
|
||||||
|
updateProfile: (updates) => {
|
||||||
|
// TODO: [BACKEND_API] PATCH /api/user/profile | Payload: Partial<UserProfile> | Expected: UserProfile | Errors: [400, 401]
|
||||||
|
set((state) => ({ profile: { ...state.profile, ...updates } }));
|
||||||
|
},
|
||||||
|
addAddress: (address) => {
|
||||||
|
// TODO: [BACKEND_API] POST /api/user/addresses | Payload: Omit<Address, "id"> | Expected: Address | Errors: [400, 401]
|
||||||
|
set((state) => ({
|
||||||
|
profile: {
|
||||||
|
...state.profile,
|
||||||
|
addresses: [...(state.profile.addresses || []), address]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
updateAddress: (id, updated) => {
|
||||||
|
// TODO: [BACKEND_API] PATCH /api/user/addresses/{id} | Payload: Address | Expected: Address | Errors: [400, 401, 404]
|
||||||
|
set((state) => ({
|
||||||
|
profile: {
|
||||||
|
...state.profile,
|
||||||
|
addresses: state.profile.addresses.map(a => a.id === id ? updated : a)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
deleteAddress: (id) => {
|
||||||
|
// TODO: [BACKEND_API] DELETE /api/user/addresses/{id} | Expected: { success: boolean } | Errors: [401, 404]
|
||||||
|
set((state) => ({
|
||||||
|
profile: {
|
||||||
|
...state.profile,
|
||||||
|
addresses: state.profile.addresses.filter(a => a.id !== id)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
setDefaultAddress: (id) => {
|
||||||
|
// TODO: [BACKEND_API] POST /api/user/addresses/{id}/set-default | Expected: { success: boolean } | Errors: [401, 404]
|
||||||
|
set((state) => ({
|
||||||
|
profile: {
|
||||||
|
...state.profile,
|
||||||
|
addresses: state.profile.addresses.map(a => ({
|
||||||
|
...a,
|
||||||
|
isDefault: a.id === id
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
topUpWallet: (amount) => {
|
||||||
|
// TODO: [BACKEND_API] POST /api/user/wallet/top-up | Payload: { amount: number } | Expected: { transactionId: string, newBalance: number } | Errors: [400, 401]
|
||||||
|
set((state) => {
|
||||||
|
const newTransaction: Transaction = {
|
||||||
|
id: `TRX-${Math.floor(Math.random() * 1000000)}`,
|
||||||
|
type: 'top_up',
|
||||||
|
amount,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
status: 'success'
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
profile: {
|
||||||
|
...state.profile,
|
||||||
|
walletBalance: state.profile.walletBalance + amount,
|
||||||
|
transactions: [newTransaction, ...(state.profile.transactions || [])]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})},
|
||||||
|
logout: () => {
|
||||||
|
// TODO: [BACKEND_API] POST /api/auth/logout | Expected: { success: boolean } | Errors: [401]
|
||||||
|
set({ role: "User_Guest", isLoggedIn: false });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ name: "canina-user" }
|
||||||
|
)
|
||||||
|
);
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
136
swagger.yml
Normal file
136
swagger.yml
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
openapi: 3.0.0
|
||||||
|
info:
|
||||||
|
title: Canina Iran API
|
||||||
|
description: API Documentation for Canina Iran Pet Health & Supplement Platform
|
||||||
|
version: 1.0.0
|
||||||
|
servers:
|
||||||
|
- url: https://api.canina.ir/v1
|
||||||
|
description: Production Server
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/auth/login:
|
||||||
|
post:
|
||||||
|
summary: User Login
|
||||||
|
tags: [Auth]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
mobile: { type: string }
|
||||||
|
role: { type: string, enum: [User_PetOwner, User_Partner] }
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Successful Login
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token: { type: string }
|
||||||
|
user: { $ref: '#/components/schemas/UserProfile' }
|
||||||
|
|
||||||
|
/products:
|
||||||
|
get:
|
||||||
|
summary: List Products
|
||||||
|
tags: [Products]
|
||||||
|
parameters:
|
||||||
|
- name: category
|
||||||
|
in: query
|
||||||
|
schema: { type: string }
|
||||||
|
- name: petType
|
||||||
|
in: query
|
||||||
|
schema: { type: string, enum: [سگ, گربه, all] }
|
||||||
|
- name: query
|
||||||
|
in: query
|
||||||
|
schema: { type: string }
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: List of products
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: array
|
||||||
|
items: { $ref: '#/components/schemas/Product' }
|
||||||
|
|
||||||
|
/pets:
|
||||||
|
post:
|
||||||
|
summary: Register new pet
|
||||||
|
tags: [Pets]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: '#/components/schemas/PetProfile' }
|
||||||
|
responses:
|
||||||
|
201:
|
||||||
|
description: Pet created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: { $ref: '#/components/schemas/PetProfile' }
|
||||||
|
|
||||||
|
/orders:
|
||||||
|
post:
|
||||||
|
summary: Place an order
|
||||||
|
tags: [Orders]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
productId: { type: string }
|
||||||
|
quantity: { type: integer }
|
||||||
|
charityDonation: { type: number }
|
||||||
|
petId: { type: string }
|
||||||
|
responses:
|
||||||
|
201:
|
||||||
|
description: Order placed
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
orderId: { type: string }
|
||||||
|
trackingNumber: { type: string }
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
UserProfile:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
firstName: { type: string }
|
||||||
|
lastName: { type: string }
|
||||||
|
email: { type: string }
|
||||||
|
walletBalance: { type: number }
|
||||||
|
charityDonationTotal: { type: number }
|
||||||
|
|
||||||
|
PetProfile:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
name: { type: string }
|
||||||
|
type: { type: string, enum: [سگ, گربه] }
|
||||||
|
breed: { type: string }
|
||||||
|
age: { type: number }
|
||||||
|
weight: { type: number }
|
||||||
|
activityLevel: { type: string }
|
||||||
|
medicalConditions:
|
||||||
|
type: array
|
||||||
|
items: { type: string }
|
||||||
|
|
||||||
|
Product:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id: { type: string }
|
||||||
|
name: { type: string }
|
||||||
|
priceValue: { type: number }
|
||||||
|
category: { type: string }
|
||||||
|
artNo: { type: string }
|
||||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
26
vite.config.ts
Normal file
26
vite.config.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
import {defineConfig, loadEnv} from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig(({mode}) => {
|
||||||
|
const env = loadEnv(mode, '.', '');
|
||||||
|
return {
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
define: {
|
||||||
|
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, '.'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||||
|
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||||
|
hmr: process.env.DISABLE_HMR !== 'true',
|
||||||
|
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
|
||||||
|
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user