feat: core pages and components migrated to Next.js (Home, Shop, Product)
This commit is contained in:
parent
72d2a5600a
commit
3de0408e6f
@ -12,12 +12,13 @@ import LoginModal from "../components/LoginModal";
|
||||
import B2BPortal from "../components/B2BPortal";
|
||||
import { useUserStore } from "../lib/store/userStore";
|
||||
import { useSettingsStore } from "../lib/store/settingsStore";
|
||||
import { useUIStore } from "../lib/store/uiStore";
|
||||
|
||||
export default function ClientLayout({ children }: { children: React.ReactNode }) {
|
||||
const [isCartOpen, setIsCartOpen] = useState(false);
|
||||
const [showB2B, setShowB2B] = useState(false);
|
||||
const [showLoginModal, setShowLoginModal] = useState(false);
|
||||
const [advisorData, setAdvisorData] = useState<any>(null);
|
||||
const {
|
||||
isCartOpen, isLoginModalOpen, isB2BPortalOpen, advisorData,
|
||||
setCartOpen, setLoginModalOpen, setB2BPortalOpen, clearAdvisorData
|
||||
} = useUIStore();
|
||||
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
@ -72,30 +73,30 @@ export default function ClientLayout({ children }: { children: React.ReactNode }
|
||||
onNavigate={handleNavigate}
|
||||
onShopNavigate={navigateToShop}
|
||||
currentView={currentView}
|
||||
onCartOpen={() => setIsCartOpen(true)}
|
||||
onCartOpen={() => setCartOpen(true)}
|
||||
onSearch={handleSearch}
|
||||
onB2BOpen={() => setShowB2B(true)}
|
||||
onB2BOpen={() => setB2BPortalOpen(true)}
|
||||
/>
|
||||
<main className="min-h-screen">
|
||||
{children}
|
||||
</main>
|
||||
<Footer onNavigate={handleNavigate} onShopNavigate={navigateToShop} onB2BOpen={() => setShowB2B(true)} />
|
||||
<Footer onNavigate={handleNavigate} onShopNavigate={navigateToShop} onB2BOpen={() => setB2BPortalOpen(true)} />
|
||||
|
||||
{showB2B && <B2BPortal onClose={() => setShowB2B(false)} />}
|
||||
{isB2BPortalOpen && <B2BPortal onClose={() => setB2BPortalOpen(false)} />}
|
||||
<CartDrawer
|
||||
isOpen={isCartOpen}
|
||||
onClose={() => setIsCartOpen(false)}
|
||||
onCheckout={() => { setIsCartOpen(false); router.push('/checkout'); }}
|
||||
onClose={() => setCartOpen(false)}
|
||||
onCheckout={() => { setCartOpen(false); router.push('/checkout'); }}
|
||||
onShopNavigate={navigateToShop}
|
||||
/>
|
||||
<LoginModal
|
||||
isOpen={showLoginModal}
|
||||
isOpen={isLoginModalOpen}
|
||||
onClose={() => {
|
||||
setShowLoginModal(false);
|
||||
setAdvisorData(null);
|
||||
setLoginModalOpen(false);
|
||||
clearAdvisorData();
|
||||
}}
|
||||
onLogin={() => {
|
||||
setShowLoginModal(false);
|
||||
setLoginModalOpen(false);
|
||||
}}
|
||||
petName={advisorData?.name}
|
||||
isAdvisorContext={!!advisorData}
|
||||
|
||||
@ -1,65 +1,116 @@
|
||||
import Image from "next/image";
|
||||
import Hero from "../components/Hero";
|
||||
import SmartAdvisor from "../components/SmartAdvisor";
|
||||
import FeaturedProducts from "../components/FeaturedProducts";
|
||||
import VetGallery from "../components/VetGallery";
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the page.tsx file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<>
|
||||
<Hero />
|
||||
{/* SmartAdvisor uses uiStore/userStore internally and handles routing automatically */}
|
||||
<SmartAdvisor />
|
||||
<FeaturedProducts />
|
||||
<VetGallery onNavigate={() => {}} />
|
||||
|
||||
{/* 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">
|
||||
<Link
|
||||
href="/shop"
|
||||
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 block"
|
||||
>
|
||||
<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>
|
||||
</Link>
|
||||
<Link
|
||||
href="/wiki"
|
||||
className="bg-medical-gray-900 p-12 rounded-[3.5rem] text-white hover:shadow-2xl hover:-translate-y-2 transition-all cursor-pointer group block"
|
||||
>
|
||||
<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>
|
||||
</Link>
|
||||
</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="/assets/images/regenerated_image_1779109861747.png"
|
||||
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">
|
||||
چرا برند آلمانی Canina مرجع دامپزشکان است؟
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
<Link
|
||||
href="/shop"
|
||||
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"
|
||||
>
|
||||
ورود به محصولات تخصصی
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
6
frontend/application/app/shop/[id]/page.tsx
Normal file
6
frontend/application/app/shop/[id]/page.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import ProductPage from "../../../components/ProductPage";
|
||||
|
||||
export default async function ShopProductPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const resolvedParams = await params;
|
||||
return <ProductPage productId={resolvedParams.id} />;
|
||||
}
|
||||
9
frontend/application/app/shop/page.tsx
Normal file
9
frontend/application/app/shop/page.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import ArchivePage from "../../components/ArchivePage";
|
||||
|
||||
export default async function Shop({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
|
||||
const resolvedParams = await searchParams;
|
||||
const category = typeof resolvedParams.category === 'string' ? resolvedParams.category : "all";
|
||||
const search = typeof resolvedParams.search === 'string' ? resolvedParams.search : "";
|
||||
|
||||
return <ArchivePage initialCategory={category} initialSearch={search} />;
|
||||
}
|
||||
@ -162,19 +162,16 @@ const ArchiveProductCard: React.FC<{ product: Product, onClick: (p: Product) =>
|
||||
);
|
||||
}
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
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 router = useRouter();
|
||||
const [selectedCategory, setSelectedCategory] = useState(initialCategory);
|
||||
const [selectedPet, setSelectedPet] = useState<PetType | "all">("all");
|
||||
const [activeSymptoms, setActiveSymptoms] = useState<string[]>([]);
|
||||
@ -262,7 +259,7 @@ export default function ArchivePage({
|
||||
{/* Mobile Navigation & Breadcrumbs */}
|
||||
<div className="lg:hidden mb-10 space-y-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
onClick={() => router.back()}
|
||||
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" />
|
||||
@ -270,7 +267,7 @@ export default function ArchivePage({
|
||||
</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>
|
||||
<span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/')}>کانینا</span>
|
||||
<ChevronLeft className="w-2.5 h-2.5" />
|
||||
<span className="text-canina-blue">فروشگاه تخصصی</span>
|
||||
</div>
|
||||
@ -278,7 +275,7 @@ export default function ArchivePage({
|
||||
|
||||
{/* 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>
|
||||
<span className="cursor-pointer hover:text-canina-blue" onClick={() => router.push('/')}>خانه</span>
|
||||
<ChevronLeft className="w-2.5 h-2.5" />
|
||||
<span className="text-canina-blue">کاتالوگ کامل محصولات</span>
|
||||
</div>
|
||||
@ -386,7 +383,7 @@ export default function ArchivePage({
|
||||
<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} />
|
||||
<ArchiveProductCard key={p.id} product={p} onClick={(p) => router.push(`/shop/${p.id}`)} />
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
@ -399,14 +396,11 @@ export default function ArchivePage({
|
||||
<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("");
|
||||
}
|
||||
router.push('/shop');
|
||||
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"
|
||||
>
|
||||
|
||||
@ -92,7 +92,10 @@ function ProductCard({ product, onClick }: { product: Product; key?: string | nu
|
||||
);
|
||||
}
|
||||
|
||||
export default function FeaturedProducts({ onProductClick, onShopNavigate }: { onProductClick: (p: Product) => void, onShopNavigate: () => void }) {
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function FeaturedProducts() {
|
||||
const router = useRouter();
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
@ -124,7 +127,7 @@ export default function FeaturedProducts({ onProductClick, onShopNavigate }: { o
|
||||
محصولات دارویی کانینا با استفاده از دانش پیشرفته بیوتکنولوژی و مواد اولیه ارگانیک نایاب فرآوری شدهاند.
|
||||
</p>
|
||||
<button
|
||||
onClick={onShopNavigate}
|
||||
onClick={() => router.push('/shop')}
|
||||
className="inline-flex items-center gap-2 text-canina-blue font-bold text-sm hover:translate-x-1 transition-transform rtl:hover:-translate-x-1"
|
||||
>
|
||||
مشاهده تمامی محصولات
|
||||
@ -138,7 +141,7 @@ export default function FeaturedProducts({ onProductClick, onShopNavigate }: { o
|
||||
Array.from({ length: 3 }).map((_, i) => <ProductCardSkeleton key={i} />)
|
||||
) : (
|
||||
products.map((p) => (
|
||||
<ProductCard key={p.id} onClick={onProductClick} product={p} />
|
||||
<ProductCard key={p.id} onClick={(p) => router.push(`/shop/${p.id}`)} product={p} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -60,7 +60,10 @@ function Typewriter({ text }: { text: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function Hero({ onProfileClick, onShopNavigate }: { onProfileClick?: () => void, onShopNavigate?: () => void }) {
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function Hero() {
|
||||
const router = useRouter();
|
||||
const getText = useSettingsStore(state => state.getText);
|
||||
const title = getText('hero_title', "تخصص آلمانی در خدمت\nسلامت پتهای خانگی");
|
||||
const titleParts = title.split('\n');
|
||||
@ -115,7 +118,7 @@ export default function Hero({ onProfileClick, onShopNavigate }: { onProfileClic
|
||||
<ChevronLeft className="w-5 h-5 group-hover:-translate-x-1 transition-transform" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onShopNavigate}
|
||||
onClick={() => router.push('/shop')}
|
||||
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"
|
||||
>
|
||||
{getText('hero_btn_products', "مشاهده محصولات")}
|
||||
|
||||
@ -67,13 +67,12 @@ const useCalculatorStore = create<CalculatorState>((set) => ({
|
||||
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;
|
||||
}) {
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function ProductPage({ productId }: { productId: string }) {
|
||||
const router = useRouter();
|
||||
const [product, setProduct] = useState<Product | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<"specs" | "feeding" | "notes">("specs");
|
||||
const [showRefillModal, setShowRefillModal] = useState(false);
|
||||
const [itemQuantity, setItemQuantity] = useState(1);
|
||||
@ -81,9 +80,14 @@ export default function ProductPage({ product, onBack, onProductClick, onWikiNav
|
||||
|
||||
useEffect(() => {
|
||||
productService.getProducts()
|
||||
.then(setAllProducts)
|
||||
.catch(err => console.error("Error fetching products in ProductPage:", err));
|
||||
}, []);
|
||||
.then((products) => {
|
||||
setAllProducts(products);
|
||||
const found = products.find(p => p.id === productId);
|
||||
setProduct(found || null);
|
||||
})
|
||||
.catch(err => console.error("Error fetching products in ProductPage:", err))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [productId]);
|
||||
const { pets, getActivePet } = usePetStore();
|
||||
const activePet = getActivePet();
|
||||
const { petType, weight, setPetType, setWeight } = useCalculatorStore();
|
||||
@ -142,9 +146,12 @@ export default function ProductPage({ product, onBack, onProductClick, onWikiNav
|
||||
|
||||
// Suggest quantity inside useEffect to avoid render-phase state update
|
||||
useEffect(() => {
|
||||
const suggestedQty = (calculation.duration < 30 && calculation.duration > 0) ? 2 : 1;
|
||||
const suggestedQty = (calculation?.duration < 30 && calculation?.duration > 0) ? 2 : 1;
|
||||
setItemQuantity(suggestedQty);
|
||||
}, [calculation.duration]);
|
||||
}, [calculation?.duration]);
|
||||
|
||||
if (isLoading) return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
|
||||
if (!product || !fullProduct || !calculation) return <div className="min-h-screen flex items-center justify-center">محصول یافت نشد</div>;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-medical-gray-50 pt-2 pb-20 px-4 md:px-0" dir="rtl">
|
||||
@ -152,7 +159,7 @@ export default function ProductPage({ product, onBack, onProductClick, onWikiNav
|
||||
{/* Mobile Header with Back Button */}
|
||||
<div className="flex items-center justify-between mb-6 md:hidden">
|
||||
<button
|
||||
onClick={onBack}
|
||||
onClick={() => router.back()}
|
||||
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" />
|
||||
@ -166,14 +173,14 @@ export default function ProductPage({ product, onBack, onProductClick, onWikiNav
|
||||
{/* 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()}
|
||||
onClick={() => router.push('/')}
|
||||
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)}
|
||||
onClick={() => router.push(`/shop?category=${product.category}`)}
|
||||
className="hover:text-canina-blue cursor-pointer font-vazir"
|
||||
>
|
||||
{product.category}
|
||||
@ -281,7 +288,7 @@ export default function ProductPage({ product, onBack, onProductClick, onWikiNav
|
||||
<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}>
|
||||
<Tooltip termKey={termKey} onWikiNavigate={(id) => router.push(`/wiki?term=${id}`)}>
|
||||
{ing}
|
||||
</Tooltip>
|
||||
) : ing}
|
||||
@ -530,7 +537,7 @@ export default function ProductPage({ product, onBack, onProductClick, onWikiNav
|
||||
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)}
|
||||
onClick={() => router.push(`/shop/${relProduct.id}`)}
|
||||
>
|
||||
<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" />
|
||||
|
||||
@ -16,16 +16,13 @@ import {
|
||||
} from "lucide-react";
|
||||
import { toPersian, cn } from "../lib/utils";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useUserStore } from "../lib/store/userStore";
|
||||
import { useUIStore } from "../lib/store/uiStore";
|
||||
import { usePetStore } from "../lib/store/usePetStore";
|
||||
|
||||
interface SmartAdvisorProps {
|
||||
onComplete: (data: {
|
||||
name: string,
|
||||
type: "سگ" | "گربه",
|
||||
breed: string,
|
||||
age: number,
|
||||
weight: number,
|
||||
activityLevel: "کم" | "متوسط" | "زیاد",
|
||||
medicalConditions: string[]
|
||||
}) => void;
|
||||
onComplete?: (data: any) => void;
|
||||
}
|
||||
|
||||
const MEDICAL_OPTIONS = [
|
||||
@ -37,7 +34,11 @@ const MEDICAL_OPTIONS = [
|
||||
{ id: "appetite", label: "بیاشتهایی", condition: "بیاشتهایی" },
|
||||
];
|
||||
|
||||
export default function SmartAdvisor({ onComplete }: SmartAdvisorProps) {
|
||||
export default function SmartAdvisor({ onComplete }: SmartAdvisorProps = {}) {
|
||||
const router = useRouter();
|
||||
const { isLoggedIn } = useUserStore();
|
||||
const setLoginModalOpen = useUIStore(state => state.setLoginModalOpen);
|
||||
const { addPet, setActivePet } = usePetStore();
|
||||
const texts = useSettingsStore(state => state.texts);
|
||||
const medicalOptions = React.useMemo(() => {
|
||||
const jsonStr = texts['medical_options'];
|
||||
@ -70,15 +71,21 @@ export default function SmartAdvisor({ onComplete }: SmartAdvisorProps) {
|
||||
const handleBack = () => setStep(s => s - 1);
|
||||
|
||||
const handleSubmit = () => {
|
||||
onComplete({
|
||||
name,
|
||||
type,
|
||||
breed,
|
||||
age,
|
||||
weight,
|
||||
activityLevel,
|
||||
medicalConditions
|
||||
});
|
||||
const data = {
|
||||
name, type, breed, age, weight, activityLevel, medicalConditions
|
||||
};
|
||||
if (onComplete) {
|
||||
onComplete(data);
|
||||
} else {
|
||||
if (!isLoggedIn) {
|
||||
setLoginModalOpen(true, data);
|
||||
} else {
|
||||
const newPet = { ...data, id: Date.now().toString() };
|
||||
addPet(newPet);
|
||||
setActivePet(newPet.id);
|
||||
router.push('/profile');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -33,7 +33,8 @@ const VIDEOS = [
|
||||
}
|
||||
];
|
||||
|
||||
export default function VetGallery({ onNavigate }: { onNavigate?: (v: any) => void }) {
|
||||
export default function VetGallery() {
|
||||
const router = useRouter();
|
||||
const [selectedVideo, setSelectedVideo] = useState<typeof VIDEOS[0] | null>(null);
|
||||
|
||||
return (
|
||||
@ -53,8 +54,8 @@ export default function VetGallery({ onNavigate }: { onNavigate?: (v: any) => vo
|
||||
</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"
|
||||
onClick={() => router.push('/videos')}
|
||||
className="inline-flex items-center gap-2 text-canina-blue font-bold text-sm hover:translate-x-1 transition-transform rtl:hover:-translate-x-1"
|
||||
>
|
||||
مشاهده همه ویدئوها
|
||||
<Play className="w-4 h-4" />
|
||||
@ -80,9 +81,9 @@ export default function VetGallery({ onNavigate }: { onNavigate?: (v: any) => vo
|
||||
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">
|
||||
<button onClick={() => router.push('/videos')} className="w-16 h-16 rounded-full bg-white/10 backdrop-blur-md flex items-center justify-center group-hover:scale-110 transition-transform border border-white/20 relative z-10">
|
||||
<PlayCircle className="w-10 h-10" />
|
||||
</div>
|
||||
</button>
|
||||
</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}
|
||||
|
||||
26
frontend/application/lib/store/uiStore.ts
Normal file
26
frontend/application/lib/store/uiStore.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface UIState {
|
||||
isCartOpen: boolean;
|
||||
isLoginModalOpen: boolean;
|
||||
isB2BPortalOpen: boolean;
|
||||
advisorData: any;
|
||||
setCartOpen: (open: boolean) => void;
|
||||
setLoginModalOpen: (open: boolean, advisorData?: any) => void;
|
||||
setB2BPortalOpen: (open: boolean) => void;
|
||||
clearAdvisorData: () => void;
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIState>((set) => ({
|
||||
isCartOpen: false,
|
||||
isLoginModalOpen: false,
|
||||
isB2BPortalOpen: false,
|
||||
advisorData: null,
|
||||
setCartOpen: (open) => set({ isCartOpen: open }),
|
||||
setLoginModalOpen: (open, data) => set((state) => ({
|
||||
isLoginModalOpen: open,
|
||||
advisorData: data !== undefined ? data : state.advisorData
|
||||
})),
|
||||
setB2BPortalOpen: (open) => set({ isB2BPortalOpen: open }),
|
||||
clearAdvisorData: () => set({ advisorData: null }),
|
||||
}));
|
||||
Loading…
Reference in New Issue
Block a user