canina/src/App.tsx
parsa aghayi c95c9e5ce0 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
2026-05-26 11:37:22 +03:30

420 lines
20 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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>
);
}