feat(ui): optimize wiki ingredient links, product images, back buttons, and checkout settings
All checks were successful
Deploy Canina / deploy (push) Successful in 1m54s
All checks were successful
Deploy Canina / deploy (push) Successful in 1m54s
This commit is contained in:
parent
03c6ed58c7
commit
2d83a00169
@ -25,10 +25,13 @@ async function bootstrap() {
|
||||
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
|
||||
// Serve static uploads folder
|
||||
// Serve static uploads folder under both /uploads/ and /api/uploads/
|
||||
app.useStaticAssets(join(process.cwd(), 'uploads'), {
|
||||
prefix: '/uploads/',
|
||||
});
|
||||
app.useStaticAssets(join(process.cwd(), 'uploads'), {
|
||||
prefix: '/api/uploads/',
|
||||
});
|
||||
|
||||
app.use(
|
||||
helmet({
|
||||
|
||||
@ -391,18 +391,9 @@ export default function ArchivePage({
|
||||
<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">
|
||||
|
||||
{/* Standard Breadcrumb Navigation & Integrated Back Button */}
|
||||
<div className="flex items-center justify-between mb-8 pb-4 border-b border-medical-gray-200/60 font-vazir gap-2">
|
||||
{/* Back Button - always visible */}
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
className="flex-shrink-0 px-3 py-1.5 bg-white border border-medical-gray-200 rounded-lg text-medical-gray-600 shadow-xs flex items-center gap-1.5 font-bold text-xs hover:border-canina-blue hover:text-canina-blue transition-all"
|
||||
>
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
<span>بازگشت</span>
|
||||
</button>
|
||||
|
||||
{/* Breadcrumb - truncates on mobile */}
|
||||
{/* Standard Breadcrumb Navigation, Mobile Filter & Integrated Back Button */}
|
||||
<div className="flex items-center justify-between mb-8 pb-4 border-b border-medical-gray-200/60 font-vazir gap-2" dir="rtl">
|
||||
{/* Breadcrumb - right side in RTL */}
|
||||
<div className="flex items-center gap-1.5 text-xs font-bold text-medical-gray-400 overflow-hidden min-w-0">
|
||||
<span className="hidden sm:inline cursor-pointer hover:text-canina-blue transition-colors shrink-0" onClick={() => router.push('/')}>خانه</span>
|
||||
<ChevronLeft className="hidden sm:block w-3 h-3 text-medical-gray-300 shrink-0" />
|
||||
@ -415,14 +406,25 @@ export default function ArchivePage({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Filter Button */}
|
||||
<button
|
||||
onClick={() => setIsMobileFilterOpen(!isMobileFilterOpen)}
|
||||
className="lg:hidden flex-shrink-0 flex items-center gap-1.5 bg-canina-blue text-white px-3 py-1.5 rounded-lg text-xs font-bold shadow-xs"
|
||||
>
|
||||
<Filter className="w-3.5 h-3.5" />
|
||||
<span>فیلترها {activeSymptoms.length > 0 || selectedPet !== "all" || selectedCategory !== "all" ? "•" : ""}</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{/* Mobile Filter Button */}
|
||||
<button
|
||||
onClick={() => setIsMobileFilterOpen(!isMobileFilterOpen)}
|
||||
className="lg:hidden flex-shrink-0 flex items-center gap-1.5 bg-canina-blue text-white px-3 py-1.5 rounded-lg text-xs font-bold shadow-xs cursor-pointer"
|
||||
>
|
||||
<Filter className="w-3.5 h-3.5" />
|
||||
<span>فیلترها {activeSymptoms.length > 0 || selectedPet !== "all" || selectedCategory !== "all" ? "•" : ""}</span>
|
||||
</button>
|
||||
|
||||
{/* Back Button - leftmost in RTL */}
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
className="flex-shrink-0 px-3 py-1.5 bg-white border border-medical-gray-200 rounded-lg text-medical-gray-600 shadow-xs flex items-center gap-1.5 font-bold text-xs hover:border-canina-blue hover:text-canina-blue transition-all cursor-pointer"
|
||||
>
|
||||
<span>بازگشت</span>
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shop Top Banner Placement */}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { ChevronLeft } from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
interface BackButtonProps {
|
||||
@ -27,12 +27,12 @@ export default function BackButton({ className, label = "بازگشت" }: BackBu
|
||||
type="button"
|
||||
onClick={handleBack}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 px-4 py-2 bg-white/80 backdrop-blur-sm border border-medical-gray-200 hover:border-canina-blue hover:text-canina-blue text-medical-gray-700 text-xs font-black rounded-xl transition-all shadow-xs active:scale-95 whitespace-nowrap cursor-pointer",
|
||||
"inline-flex items-center gap-1.5 px-4 py-2 bg-white/80 backdrop-blur-sm border border-medical-gray-200 hover:border-canina-blue hover:text-canina-blue text-medical-gray-700 text-xs font-black rounded-xl transition-all shadow-xs active:scale-95 whitespace-nowrap cursor-pointer font-vazir",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
<span>{label}</span>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { motion } from "motion/react";
|
||||
import { ChevronRight, Calendar, User, ArrowLeft, Sparkles, BookOpen, Mail, Check } from "lucide-react";
|
||||
import { ChevronRight, ChevronLeft, Calendar, User, ArrowLeft, Sparkles, BookOpen, Mail, Check } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Product } from "../lib/data/products";
|
||||
import { productService } from "../lib/services/productService";
|
||||
@ -45,10 +45,10 @@ export default function BlogPage({ blogs = [] }: { blogs: BlogPost[] }) {
|
||||
|
||||
<button
|
||||
onClick={() => setSelectedPost(null)}
|
||||
className="px-3.5 py-1.5 bg-white border border-medical-gray-200 rounded-lg text-medical-gray-600 shadow-xs flex items-center gap-1.5 font-bold text-xs hover:border-canina-blue hover:text-canina-blue transition-all whitespace-nowrap"
|
||||
className="px-3.5 py-1.5 bg-white border border-medical-gray-200 rounded-lg text-medical-gray-600 shadow-xs flex items-center gap-1.5 font-bold text-xs hover:border-canina-blue hover:text-canina-blue transition-all whitespace-nowrap cursor-pointer"
|
||||
>
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
<span>بازگشت</span>
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -168,8 +168,8 @@ export default function BlogPage({ blogs = [] }: { blogs: BlogPost[] }) {
|
||||
onClick={() => router.push('/')}
|
||||
className="px-3.5 py-1.5 bg-white border border-medical-gray-200 rounded-lg text-medical-gray-600 shadow-xs flex items-center gap-1.5 font-bold text-xs hover:border-canina-blue hover:text-canina-blue transition-all whitespace-nowrap"
|
||||
>
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
<span>بازگشت</span>
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -81,6 +81,7 @@ export default function CheckoutPage() {
|
||||
const isCardToCardEnabled = useSettingsStore((state) =>
|
||||
state.getBoolean('PAY_GATEWAY_CARD_ENABLE', true) &&
|
||||
state.getBoolean('cardToCardEnabled', true) &&
|
||||
state.getBoolean('cart_to_cart_enabled', true) &&
|
||||
state.getBoolean('payment_card_to_card_enabled', true)
|
||||
);
|
||||
const remainder = subtotal % roundStep;
|
||||
@ -261,10 +262,10 @@ export default function CheckoutPage() {
|
||||
<div className="lg:hidden mb-6 sm:mb-10 space-y-4">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="flex items-center gap-2 text-medical-gray-500 font-bold hover:text-canina-blue transition-colors"
|
||||
className="flex items-center gap-1.5 text-medical-gray-500 font-bold hover:text-canina-blue transition-colors font-vazir"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
<span>بازگشت به سبد خرید</span>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</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">
|
||||
|
||||
@ -103,9 +103,9 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
|
||||
const isLeftSwipe = distance > minSwipeDistance;
|
||||
const isRightSwipe = distance < -minSwipeDistance;
|
||||
|
||||
if (isLeftSwipe) {
|
||||
if (isRightSwipe) {
|
||||
handleNextSlide();
|
||||
} else if (isRightSwipe) {
|
||||
} else if (isLeftSwipe) {
|
||||
handlePrevSlide();
|
||||
}
|
||||
};
|
||||
|
||||
@ -161,8 +161,8 @@ export default function IngredientWiki() {
|
||||
onClick={() => router.push('/')}
|
||||
className="px-3.5 py-1.5 bg-white border border-medical-gray-200 rounded-lg text-medical-gray-600 shadow-xs flex items-center gap-1.5 font-bold text-xs hover:border-canina-blue hover:text-canina-blue transition-all whitespace-nowrap cursor-pointer"
|
||||
>
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
<span>بازگشت</span>
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -243,12 +243,12 @@ export default function IngredientWiki() {
|
||||
<Beaker className="w-6 h-6 text-canina-gold" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl sm:text-3xl font-black text-medical-gray-900 leading-tight">{ing.name}</h3>
|
||||
<span className="text-[11px] text-canina-blue font-mono font-bold uppercase tracking-wider">کد دارویی: {ing.id}</span>
|
||||
<h3 className="text-2xl sm:text-3xl font-black text-medical-gray-900 leading-tight font-vazir">{ing.name}</h3>
|
||||
<span className="text-[12px] text-canina-blue font-bold tracking-wider font-vazir inline-block mt-1 bg-canina-blue/5 px-2.5 py-0.5 rounded-lg border border-canina-blue/10">کد ماده مؤثره: <span className="font-mono">{ing.id}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-base text-medical-gray-600 leading-relaxed mb-6">
|
||||
<p className="text-base text-medical-gray-600 leading-relaxed mb-6 font-vazir">
|
||||
{ing.description}
|
||||
</p>
|
||||
|
||||
@ -256,7 +256,7 @@ export default function IngredientWiki() {
|
||||
{ing.benefits.map((benefit, bIdx) => (
|
||||
<div key={bIdx} className="flex items-start gap-3 p-3.5 bg-medical-gray-50/80 rounded-2xl border border-medical-gray-100">
|
||||
<CheckCircle2 className="w-5 h-5 text-canina-blue shrink-0 mt-0.5" />
|
||||
<span className="text-xs sm:text-sm font-bold text-medical-gray-700 leading-relaxed">{benefit}</span>
|
||||
<span className="text-xs sm:text-sm font-bold text-medical-gray-700 leading-relaxed font-vazir">{benefit}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -277,9 +277,12 @@ export default function IngredientWiki() {
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-canina-blue animate-pulse" />
|
||||
<h4 className="text-xs sm:text-sm font-black text-medical-gray-900 font-vazir">موجود در محصولات کاتالوگ</h4>
|
||||
</div>
|
||||
<span className="text-xs font-black text-canina-blue bg-canina-blue/10 px-3 py-1 rounded-full font-vazir">
|
||||
<Link
|
||||
href={`/shop?search=${encodeURIComponent(ing.name.replace(/\s*\([^)]*\)\s*/g, '').trim())}`}
|
||||
className="text-xs font-black text-canina-blue bg-canina-blue/10 hover:bg-canina-blue hover:text-white px-3 py-1 rounded-full font-vazir transition-all"
|
||||
>
|
||||
{hasIngredient.length} محصول
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 relative z-10">
|
||||
@ -290,16 +293,24 @@ export default function IngredientWiki() {
|
||||
className="bg-white rounded-[2rem] p-4 shadow-sm border border-medical-gray-200 hover:border-canina-blue hover:shadow-lg hover:scale-[1.02] transition-all cursor-pointer group flex flex-col justify-between"
|
||||
>
|
||||
<div className="aspect-square bg-medical-gray-50 rounded-2xl p-3 mb-3 flex items-center justify-center relative overflow-hidden">
|
||||
<Image src={p.image} alt={p.name} width={96} height={96} className="max-h-20 w-auto object-contain group-hover:scale-110 transition-transform duration-300" unoptimized />
|
||||
<Image
|
||||
src={p.image || '/assets/images/canina-product-placeholder.png'}
|
||||
alt={p.name}
|
||||
width={96}
|
||||
height={96}
|
||||
className="max-h-20 w-auto object-contain group-hover:scale-110 transition-transform duration-300"
|
||||
unoptimized
|
||||
onError={(e: any) => { e.currentTarget.src = '/assets/images/canina-product-placeholder.png'; }}
|
||||
/>
|
||||
</div>
|
||||
<h4 className="text-xs font-black text-center text-medical-gray-900 group-hover:text-canina-blue transition-colors px-1 leading-snug font-vazir line-clamp-2">{p.name}</h4>
|
||||
</Link>
|
||||
)) : (
|
||||
<div className="col-span-full py-8 text-center">
|
||||
<p className="text-medical-gray-400 font-bold italic mb-3">محصولی با این ماده مؤثره یافت نشد.</p>
|
||||
<p className="text-medical-gray-400 font-bold italic mb-3 font-vazir">محصولی با این ماده مؤثره یافت نشد.</p>
|
||||
<button
|
||||
onClick={() => router.push('/shop')}
|
||||
className="text-canina-blue font-black text-sm hover:underline cursor-pointer"
|
||||
className="text-canina-blue font-black text-sm hover:underline cursor-pointer font-vazir"
|
||||
>
|
||||
مشاهده همه محصولات
|
||||
</button>
|
||||
@ -310,9 +321,13 @@ export default function IngredientWiki() {
|
||||
|
||||
{hasIngredient.length > 4 && (
|
||||
<div className="mt-4 pt-3 border-t border-medical-gray-200/50 text-center">
|
||||
<span className="text-[11px] font-black text-medical-gray-500">
|
||||
+ و {hasIngredient.length - 4} محصول تخصصی دیگر در کاتالوگ
|
||||
</span>
|
||||
<Link
|
||||
href={`/shop?search=${encodeURIComponent(ing.name.replace(/\s*\([^)]*\)\s*/g, '').trim())}`}
|
||||
className="text-xs font-black text-canina-blue hover:text-medical-gray-900 transition-colors font-vazir inline-flex items-center gap-1.5"
|
||||
>
|
||||
<span>+ و {hasIngredient.length - 4} محصول تخصصی دیگر در کاتالوگ</span>
|
||||
<span className="underline">مشاهده همه</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import {
|
||||
X,
|
||||
@ -24,6 +25,7 @@ import {
|
||||
import { toPersian } from "../lib/utils";
|
||||
import api from "../lib/services/api";
|
||||
import { toast } from "sonner";
|
||||
import { useSettingsStore } from "../lib/store/settingsStore";
|
||||
|
||||
interface OrderDetailsModalProps {
|
||||
isOpen: boolean;
|
||||
@ -38,9 +40,13 @@ export default function OrderDetailsModal({
|
||||
order,
|
||||
onOrderUpdated,
|
||||
}: OrderDetailsModalProps) {
|
||||
const getText = useSettingsStore((state) => state.getText);
|
||||
const isCardToCardEnabled = getText('PAY_GATEWAY_CARD_ENABLE', 'true') === 'true' && getText('cart_to_cart_enabled', 'true') === 'true';
|
||||
|
||||
const [retryModalOpen, setRetryModalOpen] = useState(false);
|
||||
const [selectedMethod, setSelectedMethod] = useState<'online' | 'wallet' | 'card'>('online');
|
||||
const [isProcessingPayment, setIsProcessingPayment] = useState(false);
|
||||
const [zoomedImage, setZoomedImage] = useState<string | null>(null);
|
||||
|
||||
if (!order) return null;
|
||||
|
||||
@ -56,6 +62,8 @@ export default function OrderDetailsModal({
|
||||
order.trackingNumber || (typeof order.id === 'string' ? order.id.substring(0, 8) : '') || 'CN-0000'
|
||||
);
|
||||
|
||||
const shippingAddress = (order.shippingAddress || order.address || {}) as Record<string, unknown>;
|
||||
|
||||
const rawDate = order.date || order.createdAt;
|
||||
const orderDate = rawDate ? new Date(String(rawDate)) : new Date();
|
||||
const formattedDate = orderDate.toLocaleDateString("fa-IR");
|
||||
@ -390,298 +398,328 @@ export default function OrderDetailsModal({
|
||||
<span className="text-xs sm:text-sm font-black text-medical-gray-900">
|
||||
{getStatusLabel(orderStatus)}
|
||||
</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-xs sm:text-sm font-bold text-canina-blue font-vazir">
|
||||
{toPersian(displayTrackingCode)}
|
||||
</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-1.5">
|
||||
<Clock className="w-4 h-4 text-medical-gray-400 flex-shrink-0" />
|
||||
<span className="text-xs sm:text-sm font-black text-medical-gray-900">
|
||||
{toPersian(formattedTime)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Items List */}
|
||||
<div>
|
||||
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
|
||||
<ShoppingBag className="w-4 h-4 text-canina-blue" />
|
||||
سبد اقلام سفارش
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{items.map((item: Record<string, unknown>, idx: number) => {
|
||||
const prod = item.product as Record<string, unknown> | undefined;
|
||||
const pName = getProductName(prod);
|
||||
const pImg = getProductImage(prod);
|
||||
const pPrice = Number(prod?.priceValue || item.priceValue || 0);
|
||||
const itemQty = Number(item.quantity || 1);
|
||||
return (
|
||||
<div
|
||||
key={String(prod?.id || idx)}
|
||||
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-14 h-14 bg-medical-gray-50 rounded-xl p-1.5 flex items-center justify-center shrink-0 border border-medical-gray-200">
|
||||
<img
|
||||
src={pImg}
|
||||
alt={pName}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
|
||||
<ShoppingBag className="w-4 h-4 text-canina-blue" />
|
||||
سبد اقلام سفارش
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{items.map((item: Record<string, unknown>, idx: number) => {
|
||||
const prod = item.product as Record<string, unknown> | undefined;
|
||||
const pName = getProductName(prod);
|
||||
const pImg = getProductImage(prod);
|
||||
const pPrice = Number(prod?.priceValue || item.priceValue || 0);
|
||||
const itemQty = Number(item.quantity || 1);
|
||||
const pSlug = String(prod?.slug || prod?.id || '');
|
||||
return (
|
||||
<div
|
||||
key={String(prod?.id || idx)}
|
||||
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
|
||||
onClick={() => setZoomedImage(pImg)}
|
||||
className="w-14 h-14 bg-medical-gray-50 rounded-xl p-1.5 flex items-center justify-center shrink-0 border border-medical-gray-200 cursor-zoom-in hover:scale-105 transition-transform"
|
||||
title="مشاهده تصویر بزرگ"
|
||||
>
|
||||
<img
|
||||
src={pImg}
|
||||
alt={pName}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
onError={(e: any) => { e.currentTarget.src = '/assets/images/canina-product-placeholder.png'; }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{pSlug ? (
|
||||
<Link
|
||||
href={`/shop/${pSlug}`}
|
||||
onClick={onClose}
|
||||
className="font-black text-medical-gray-900 hover:text-canina-blue transition-colors text-xs sm:text-sm inline-block font-vazir"
|
||||
>
|
||||
{pName}
|
||||
</Link>
|
||||
) : (
|
||||
<h5 className="font-black text-medical-gray-900 text-xs sm:text-sm font-vazir">
|
||||
{pName}
|
||||
</h5>
|
||||
)}
|
||||
<p className="text-xs text-medical-gray-400 mt-0.5 font-vazir">
|
||||
{toPersian(itemQty)} عدد × {toPersian(pPrice.toLocaleString())} تومان
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-left font-black text-canina-blue text-xs sm:text-sm font-vazir">
|
||||
{toPersian((pPrice * itemQty).toLocaleString())} تومان
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shipping & Payment */}
|
||||
<div className="grid md:grid-cols-2 gap-6 pt-4 border-t border-medical-gray-100">
|
||||
<div>
|
||||
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4 text-canina-blue" />
|
||||
اطلاعات تحویل و آدرس
|
||||
</h4>
|
||||
<div className="bg-medical-gray-50 p-4 rounded-2xl space-y-2 text-xs font-bold text-medical-gray-700">
|
||||
<div>
|
||||
<span className="text-medical-gray-400">گیرنده: </span>
|
||||
{String(shippingAddress.recipientName || shippingAddress.name || 'کاربر')}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-medical-gray-400">شماره تماس: </span>
|
||||
<span className="font-mono">{toPersian(String(shippingAddress.phone || shippingAddress.mobile || '-'))}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-medical-gray-400">نشانی کامل: </span>
|
||||
{String(shippingAddress.fullAddress || shippingAddress.address || '-')}
|
||||
</div>
|
||||
{Boolean(shippingAddress.postalCode) && (
|
||||
<div>
|
||||
<h5 className="font-black text-medical-gray-900 text-xs sm:text-sm">
|
||||
{pName}
|
||||
</h5>
|
||||
<p className="text-xs text-medical-gray-400 mt-0.5">
|
||||
{toPersian(itemQty)} عدد × {toPersian(pPrice.toLocaleString())} تومان
|
||||
</p>
|
||||
<span className="text-medical-gray-400">کد پستی: </span>
|
||||
<span className="font-mono">{toPersian(String(shippingAddress.postalCode))}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-left font-black text-canina-blue text-xs sm:text-sm">
|
||||
{toPersian((pPrice * itemQty).toLocaleString())} تومان
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shipping & Payment */}
|
||||
<div className="grid md:grid-cols-2 gap-6 pt-4 border-t border-medical-gray-100">
|
||||
<div>
|
||||
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4 text-canina-blue" />
|
||||
اطلاعات ارسال
|
||||
</h4>
|
||||
<p className="text-xs font-bold text-medical-gray-600 leading-relaxed bg-medical-gray-50 p-4 rounded-2xl border border-medical-gray-100">
|
||||
{String(order.shippingAddress || 'آدرس ثبتی کاربر در حساب کاربری')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
|
||||
<CreditCard className="w-4 h-4 text-canina-blue" />
|
||||
جزئیات مالی و روش پرداخت
|
||||
</h4>
|
||||
<div className="space-y-2 bg-medical-gray-50 p-4 rounded-2xl border border-medical-gray-100">
|
||||
<div className="flex justify-between text-xs font-bold text-medical-gray-500">
|
||||
<span>روش پرداخت:</span>
|
||||
<span className="text-medical-gray-900 font-black">
|
||||
{getPaymentMethodTitle(order.paymentMethod)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs font-bold text-medical-gray-500">
|
||||
<span>مجموع اولیه اقلام:</span>
|
||||
<span>{toPersian(itemsSubtotal.toLocaleString())} تومان</span>
|
||||
</div>
|
||||
{charityAmount > 0 && (
|
||||
<div className="flex justify-between text-xs font-bold text-pink-500">
|
||||
<span>کمک اهدایی (ردپای مهربانی):</span>
|
||||
<span>{toPersian(charityAmount.toLocaleString())}+ تومان</span>
|
||||
<div>
|
||||
<h4 className="text-sm font-black text-medical-gray-900 mb-3 flex items-center gap-2">
|
||||
<CreditCard className="w-4 h-4 text-canina-blue" />
|
||||
اطلاعات و وضعیت پرداخت
|
||||
</h4>
|
||||
<div className="bg-medical-gray-50 p-4 rounded-2xl space-y-2.5 text-xs font-bold text-medical-gray-700">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-medical-gray-400">شیوه پرداخت:</span>
|
||||
<span className="text-medical-gray-900">{getPaymentMethodTitle(order.paymentMethod)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-medical-gray-400">مجموع اقلام:</span>
|
||||
<span>{toPersian(itemsSubtotal.toLocaleString())} تومان</span>
|
||||
</div>
|
||||
{charityAmount > 0 && (
|
||||
<div className="flex justify-between items-center text-pink-500">
|
||||
<span>ردپای مهربانی:</span>
|
||||
<span>{toPersian(charityAmount.toLocaleString())}+ تومان</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-medical-gray-400">هزینه ارسال:</span>
|
||||
<span className="text-green-600">رایگان</span>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-medical-gray-200/60 flex justify-between items-center text-sm font-black text-medical-gray-900">
|
||||
<span>مبلغ نهایی فاکتور:</span>
|
||||
<span className="text-canina-blue">{toPersian(totalAmount.toLocaleString())} تومان</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="p-6 sm:px-10 bg-medical-gray-50 border-t border-medical-gray-100 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="w-full sm:w-auto px-5 py-2.5 bg-white border border-medical-gray-200 text-medical-gray-700 rounded-xl text-xs font-black hover:border-canina-blue hover:text-canina-blue transition-all flex items-center justify-center gap-2 shadow-xs cursor-pointer"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>چاپ فاکتور رسمی</span>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-3 w-full sm:w-auto">
|
||||
{isPendingPayment && (
|
||||
<button
|
||||
onClick={() => setRetryModalOpen(true)}
|
||||
className="w-full sm:w-auto px-6 py-2.5 bg-green-600 hover:bg-green-700 text-white rounded-xl text-xs font-black shadow-lg shadow-green-600/20 transition-all flex items-center justify-center gap-2 cursor-pointer"
|
||||
>
|
||||
<CreditCard className="w-4 h-4" />
|
||||
<span>پرداخت سفارش</span>
|
||||
</button>
|
||||
)}
|
||||
<div className="flex justify-between text-xs font-bold text-medical-gray-500">
|
||||
<span>هزینه ارسال:</span>
|
||||
<span className="text-green-600 font-black">رایگان</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm sm:text-base font-black text-medical-gray-900 pt-2 border-t border-medical-gray-200">
|
||||
<span>مبلغ نهایی سفارش:</span>
|
||||
<span className="text-canina-blue">
|
||||
{toPersian(totalAmount.toLocaleString())} تومان
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full sm:w-auto px-6 py-2.5 bg-medical-gray-900 hover:bg-canina-blue text-white rounded-xl text-xs font-black transition-all cursor-pointer flex items-center justify-center gap-1.5"
|
||||
>
|
||||
<span>بازگشت</span>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="p-6 sm:p-8 bg-medical-gray-50 border-t border-medical-gray-100 flex flex-col sm:flex-row gap-3">
|
||||
{isPendingPayment && (
|
||||
<button
|
||||
onClick={() => setRetryModalOpen(true)}
|
||||
className="flex-1 py-3.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-2xl font-black text-xs sm:text-sm flex items-center justify-center gap-2 shadow-lg shadow-emerald-600/20 transition-all"
|
||||
>
|
||||
<CreditCard className="w-4 h-4" />
|
||||
<span>پرداخت آنلاین و تکمیل سفارش</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="flex-1 py-3.5 bg-canina-blue hover:bg-medical-gray-900 text-white rounded-2xl font-black text-xs sm:text-sm flex items-center justify-center gap-2 transition-all shadow-md"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>دانلود و چاپ فاکتور رسمی</span>
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Retry Payment Method Selector Modal */}
|
||||
{retryModalOpen && (
|
||||
<div className="fixed inset-0 z-60 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-3xl p-6 sm:p-8 w-full max-w-md shadow-2xl space-y-6 font-vazir animate-in fade-in zoom-in-95 duration-150" dir="rtl">
|
||||
<div className="flex items-center justify-between border-b border-medical-gray-100 pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<CreditCard className="w-5 h-5 text-canina-blue" />
|
||||
{/* Retry Payment Modal */}
|
||||
{retryModalOpen && (
|
||||
<div className="fixed inset-0 z-60 bg-black/60 backdrop-blur-xs flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-3xl p-6 sm:p-8 max-w-md w-full shadow-2xl border border-medical-gray-200 space-y-6">
|
||||
<div className="flex items-center justify-between pb-4 border-b border-medical-gray-100">
|
||||
<h3 className="text-base font-black text-medical-gray-900">
|
||||
انتخاب روش پرداخت سفارش
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setRetryModalOpen(false)}
|
||||
className="text-medical-gray-400 hover:text-medical-gray-600"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setRetryModalOpen(false)}
|
||||
className="p-1 text-medical-gray-400 hover:text-medical-gray-600 rounded-lg hover:bg-medical-gray-100"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-3.5 bg-canina-blue/5 border border-canina-blue/10 rounded-2xl flex justify-between items-center text-xs">
|
||||
<span className="font-bold text-medical-gray-600">مبلغ قابل پرداخت:</span>
|
||||
<span className="font-black text-canina-blue text-sm">
|
||||
{toPersian(totalAmount.toLocaleString())} تومان
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Methods Options */}
|
||||
<div className="space-y-3">
|
||||
<label
|
||||
onClick={() => setSelectedMethod('online')}
|
||||
className={`flex items-center justify-between p-4 rounded-2xl border-2 cursor-pointer transition-all ${
|
||||
selectedMethod === 'online'
|
||||
? 'border-canina-blue bg-canina-blue/5'
|
||||
: 'border-medical-gray-100 hover:border-medical-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-emerald-50 text-emerald-600 rounded-xl flex items-center justify-center font-bold">
|
||||
<CreditCard className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-black text-medical-gray-900">
|
||||
درگاه پرداخت آنلاین زیبال
|
||||
</div>
|
||||
<div className="text-[10px] text-medical-gray-400 font-bold mt-0.5">
|
||||
پرداخت آنی با تمامی کارتهای بانکی عضو شتاب
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
||||
<div className="space-y-3">
|
||||
<label
|
||||
onClick={() => setSelectedMethod('online')}
|
||||
className={`flex items-center justify-between p-4 rounded-2xl border-2 cursor-pointer transition-all ${
|
||||
selectedMethod === 'online'
|
||||
? 'border-canina-blue bg-canina-blue'
|
||||
: 'border-medical-gray-300'
|
||||
? 'border-canina-blue bg-canina-blue/5'
|
||||
: 'border-medical-gray-100 hover:border-medical-gray-200'
|
||||
}`}
|
||||
>
|
||||
{selectedMethod === 'online' && (
|
||||
<span className="w-2 h-2 rounded-full bg-white" />
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-green-50 text-green-600 rounded-xl flex items-center justify-center font-bold">
|
||||
<CreditCard className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-black text-medical-gray-900">
|
||||
درگاه آنلاین بانکی
|
||||
</div>
|
||||
<div className="text-[10px] text-medical-gray-400 font-bold mt-0.5">
|
||||
زیبال / کلیه کارتهای عضو شتاب
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
||||
selectedMethod === 'online'
|
||||
? 'border-canina-blue bg-canina-blue'
|
||||
: 'border-medical-gray-300'
|
||||
}`}
|
||||
>
|
||||
{selectedMethod === 'online' && (
|
||||
<span className="w-2 h-2 rounded-full bg-white" />
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
onClick={() => setSelectedMethod('wallet')}
|
||||
className={`flex items-center justify-between p-4 rounded-2xl border-2 cursor-pointer transition-all ${
|
||||
selectedMethod === 'wallet'
|
||||
? 'border-canina-blue bg-canina-blue/5'
|
||||
: 'border-medical-gray-100 hover:border-medical-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-purple-50 text-purple-600 rounded-xl flex items-center justify-center font-bold">
|
||||
<Wallet className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-black text-medical-gray-900">
|
||||
کیف پول الکترونیک کنینا
|
||||
</div>
|
||||
<div className="text-[10px] text-medical-gray-400 font-bold mt-0.5">
|
||||
کسر خودکار از موجودی کیف پول
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
||||
<label
|
||||
onClick={() => setSelectedMethod('wallet')}
|
||||
className={`flex items-center justify-between p-4 rounded-2xl border-2 cursor-pointer transition-all ${
|
||||
selectedMethod === 'wallet'
|
||||
? 'border-canina-blue bg-canina-blue'
|
||||
: 'border-medical-gray-300'
|
||||
? 'border-canina-blue bg-canina-blue/5'
|
||||
: 'border-medical-gray-100 hover:border-medical-gray-200'
|
||||
}`}
|
||||
>
|
||||
{selectedMethod === 'wallet' && (
|
||||
<span className="w-2 h-2 rounded-full bg-white" />
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label
|
||||
onClick={() => setSelectedMethod('card')}
|
||||
className={`flex items-center justify-between p-4 rounded-2xl border-2 cursor-pointer transition-all ${
|
||||
selectedMethod === 'card'
|
||||
? 'border-canina-blue bg-canina-blue/5'
|
||||
: 'border-medical-gray-100 hover:border-medical-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-50 text-blue-600 rounded-xl flex items-center justify-center font-bold">
|
||||
<Building2 className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-black text-medical-gray-900">
|
||||
کارت به کارت مستقیم
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-amber-50 text-amber-600 rounded-xl flex items-center justify-center font-bold">
|
||||
<Wallet className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="text-[10px] text-medical-gray-400 font-bold mt-0.5">
|
||||
واریز به شماره کارت / شبا شرکت
|
||||
<div>
|
||||
<div className="text-xs font-black text-medical-gray-900">
|
||||
کیف پول حساب کاربری
|
||||
</div>
|
||||
<div className="text-[10px] text-medical-gray-400 font-bold mt-0.5">
|
||||
کسر مستقیم از موجودی کیف پول
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
||||
selectedMethod === 'card'
|
||||
? 'border-canina-blue bg-canina-blue'
|
||||
: 'border-medical-gray-300'
|
||||
}`}
|
||||
>
|
||||
{selectedMethod === 'card' && (
|
||||
<span className="w-2 h-2 rounded-full bg-white" />
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
||||
selectedMethod === 'wallet'
|
||||
? 'border-canina-blue bg-canina-blue'
|
||||
: 'border-medical-gray-300'
|
||||
}`}
|
||||
>
|
||||
{selectedMethod === 'wallet' && (
|
||||
<span className="w-2 h-2 rounded-full bg-white" />
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRetryModalOpen(false)}
|
||||
className="px-4 py-2.5 text-xs font-bold text-medical-gray-600 hover:bg-medical-gray-100 rounded-xl transition-all"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isProcessingPayment}
|
||||
onClick={handleExecuteRetryPayment}
|
||||
className="px-6 py-2.5 text-xs font-black bg-canina-blue hover:bg-canina-dark text-white rounded-xl shadow-lg shadow-canina-blue/20 transition-all flex items-center gap-2"
|
||||
>
|
||||
{isProcessingPayment ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
{isCardToCardEnabled && (
|
||||
<label
|
||||
onClick={() => setSelectedMethod('card')}
|
||||
className={`flex items-center justify-between p-4 rounded-2xl border-2 cursor-pointer transition-all ${
|
||||
selectedMethod === 'card'
|
||||
? 'border-canina-blue bg-canina-blue/5'
|
||||
: 'border-medical-gray-100 hover:border-medical-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-50 text-blue-600 rounded-xl flex items-center justify-center font-bold">
|
||||
<Building2 className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-black text-medical-gray-900">
|
||||
کارت به کارت مستقیم
|
||||
</div>
|
||||
<div className="text-[10px] text-medical-gray-400 font-bold mt-0.5">
|
||||
واریز به شماره کارت / شبا شرکت
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
||||
selectedMethod === 'card'
|
||||
? 'border-canina-blue bg-canina-blue'
|
||||
: 'border-medical-gray-300'
|
||||
}`}
|
||||
>
|
||||
{selectedMethod === 'card' && (
|
||||
<span className="w-2 h-2 rounded-full bg-white" />
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
<span>تایید و ادامه پرداخت</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRetryModalOpen(false)}
|
||||
className="px-4 py-2.5 text-xs font-bold text-medical-gray-600 hover:bg-medical-gray-100 rounded-xl transition-all"
|
||||
>
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isProcessingPayment}
|
||||
onClick={handleExecuteRetryPayment}
|
||||
className="px-6 py-2.5 text-xs font-black bg-canina-blue hover:bg-canina-dark text-white rounded-xl shadow-lg shadow-canina-blue/20 transition-all flex items-center gap-2"
|
||||
>
|
||||
{isProcessingPayment ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
)}
|
||||
<span>تایید و ادامه پرداخت</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Image Zoom Modal */}
|
||||
{zoomedImage && (
|
||||
<div
|
||||
className="fixed inset-0 z-70 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
onClick={() => setZoomedImage(null)}
|
||||
>
|
||||
<div className="relative max-w-lg w-full bg-white p-6 rounded-3xl shadow-2xl flex flex-col items-center">
|
||||
<button
|
||||
onClick={() => setZoomedImage(null)}
|
||||
className="absolute top-4 left-4 p-2 rounded-full bg-medical-gray-100 hover:bg-medical-gray-200 text-medical-gray-700 transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
<img
|
||||
src={zoomedImage}
|
||||
alt="نمایش کامل تصویر محصول"
|
||||
className="max-h-[70vh] w-auto object-contain rounded-2xl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@ -43,10 +43,10 @@ export default function OrderTracking() {
|
||||
<div className="lg:hidden mb-12 space-y-4">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="flex items-center gap-2 text-medical-gray-500 font-bold hover:text-canina-blue transition-colors"
|
||||
className="flex items-center gap-1.5 text-medical-gray-500 font-bold hover:text-canina-blue transition-colors font-vazir"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5 font-vazir" />
|
||||
<span>بازگشت</span>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</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">
|
||||
|
||||
@ -78,8 +78,20 @@ export default function PetProfile({ initialView, advisorNeed, embedded = false
|
||||
}
|
||||
}, [initialView]);
|
||||
|
||||
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
|
||||
const [activePetTab, setActivePetTab] = useState<"health" | "orders" | "prescriptions">("health");
|
||||
|
||||
// URL query param retention for pet and tab
|
||||
useEffect(() => {
|
||||
const petParam = searchParams.get('pet');
|
||||
if (petParam && pets.length > 0) {
|
||||
const matchedPet = pets.find(p => p.id === petParam || p.name === petParam);
|
||||
if (matchedPet && matchedPet.id !== activePet?.id) {
|
||||
setActivePet(matchedPet.id);
|
||||
}
|
||||
}
|
||||
}, [searchParams, pets, activePet?.id, setActivePet]);
|
||||
|
||||
// URL query param retention for tab
|
||||
useEffect(() => {
|
||||
const tabParam = searchParams.get('tab');
|
||||
@ -365,7 +377,15 @@ export default function PetProfile({ initialView, advisorNeed, embedded = false
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => { setActivePet(pet.id); setView("detail"); }}
|
||||
onClick={() => {
|
||||
setActivePet(pet.id);
|
||||
setView("detail");
|
||||
if (typeof window !== 'undefined') {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('pet', pet.id);
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
}}
|
||||
className="w-full py-4 bg-medical-gray-900 text-white rounded-2xl font-black hover:bg-canina-blue transition-all flex items-center justify-center gap-2 group-hover:shadow-lg"
|
||||
>
|
||||
مشاهده شناسنامه سلامت
|
||||
@ -523,47 +543,93 @@ export default function PetProfile({ initialView, advisorNeed, embedded = false
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[10px] font-black text-medical-gray-400 uppercase tracking-widest mb-3 pr-2">تصویر یا عکس پت</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={formData.imageUrl || ''}
|
||||
onChange={e => setFormData({ ...formData, imageUrl: e.target.value })}
|
||||
className="flex-1 bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 px-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs font-mono"
|
||||
placeholder="آدرس یا لینک عکس..."
|
||||
/>
|
||||
<label className="bg-canina-blue text-white px-4 py-3 rounded-2xl text-xs font-bold cursor-pointer hover:bg-canina-dark transition-all flex items-center justify-center shrink-0">
|
||||
آپلود فایل
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
try {
|
||||
const res = await api.post('/pets/upload-image', fd, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
const url = res.data?.url || res.data?.path || res.data?.data?.url;
|
||||
if (url) {
|
||||
setFormData({ ...formData, imageUrl: url });
|
||||
toast.success("تصویر با موفقیت آپلود شد");
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
const res = await api.post('/upload', fd, { headers: { 'Content-Type': 'multipart/form-data' } });
|
||||
const url = res.data?.url || res.data?.path || res.data?.data?.url;
|
||||
if (url) {
|
||||
setFormData({ ...formData, imageUrl: url });
|
||||
toast.success("تصویر با موفقیت آپلود شد");
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4 bg-medical-gray-50 p-4 rounded-2xl border border-medical-gray-200">
|
||||
{/* Photo preview container */}
|
||||
<div className="relative w-20 h-20 rounded-2xl overflow-hidden bg-medical-gray-200 border-2 border-dashed border-medical-gray-300 flex items-center justify-center shrink-0">
|
||||
{formData.imageUrl ? (
|
||||
<img
|
||||
src={formData.imageUrl.startsWith('http') ? formData.imageUrl : `${BASE_DOMAIN}${formData.imageUrl}`}
|
||||
alt="عکس پت"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Dog className="w-8 h-8 text-medical-gray-400" />
|
||||
)}
|
||||
|
||||
{uploadProgress !== null && (
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-xs flex flex-col items-center justify-center text-white">
|
||||
<div className="relative w-10 h-10 flex items-center justify-center">
|
||||
<svg className="w-full h-full -rotate-90" viewBox="0 0 36 36">
|
||||
<path
|
||||
className="text-white/20"
|
||||
strokeWidth="3"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
/>
|
||||
<path
|
||||
className="text-canina-blue"
|
||||
strokeDasharray={`${uploadProgress}, 100`}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
/>
|
||||
</svg>
|
||||
<span className="absolute text-[9px] font-black">{toPersian(uploadProgress)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 w-full space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={formData.imageUrl || ''}
|
||||
onChange={e => setFormData({ ...formData, imageUrl: e.target.value })}
|
||||
className="flex-1 bg-white border border-medical-gray-200 rounded-xl py-2.5 px-3 focus:ring-2 focus:ring-canina-blue/20 outline-none text-xs font-mono"
|
||||
placeholder="آدرس اینترنتی تصویر..."
|
||||
/>
|
||||
<label className="bg-canina-blue text-white px-4 py-2.5 rounded-xl text-xs font-black cursor-pointer hover:bg-canina-dark transition-all flex items-center justify-center shrink-0 shadow-xs">
|
||||
انتخاب تصویر
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
setUploadProgress(10);
|
||||
try {
|
||||
const res = await api.post('/pets/upload-image', fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
onUploadProgress: (progressEvent) => {
|
||||
const total = progressEvent.total || file.size;
|
||||
const percent = Math.round((progressEvent.loaded * 100) / total);
|
||||
setUploadProgress(percent);
|
||||
}
|
||||
});
|
||||
const url = res.data?.url || res.data?.path || res.data?.data?.url;
|
||||
if (url) {
|
||||
setFormData({ ...formData, imageUrl: url });
|
||||
toast.success("تصویر با موفقیت آپلود شد");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Pet image upload error:", err);
|
||||
toast.error("خطا در آپلود تصویر");
|
||||
} finally {
|
||||
setTimeout(() => setUploadProgress(null), 500);
|
||||
}
|
||||
} catch {
|
||||
toast.error("خطا در آپلود تصویر");
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-[10px] text-medical-gray-400 font-bold">فرمتهای مجاز: JPG, PNG (حداکثر ۱۰ مگابایت)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
@ -724,18 +790,19 @@ export default function PetProfile({ initialView, advisorNeed, embedded = false
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => setView("index")}
|
||||
className="px-6 py-2.5 bg-white border border-medical-gray-200 rounded-xl text-[10px] font-black uppercase tracking-widest hover:border-canina-blue transition-all"
|
||||
>
|
||||
بازگشت به لیست
|
||||
</button>
|
||||
<button
|
||||
onClick={startEdit}
|
||||
className="px-6 py-2.5 bg-canina-blue text-white rounded-xl text-[10px] font-black uppercase tracking-widest shadow-lg shadow-canina-blue/20 hover:scale-105 transition-all flex items-center gap-2"
|
||||
className="px-6 py-2.5 bg-canina-blue text-white rounded-xl text-xs font-black shadow-lg shadow-canina-blue/20 hover:scale-105 transition-all flex items-center gap-2"
|
||||
>
|
||||
<Edit3 className="w-3.5 h-3.5" />
|
||||
ویرایش پرونده
|
||||
<span>ویرایش پرونده</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView("index")}
|
||||
className="px-6 py-2.5 bg-white border border-medical-gray-200 rounded-xl text-xs font-black text-medical-gray-700 hover:border-canina-blue hover:text-canina-blue transition-all flex items-center gap-1.5"
|
||||
>
|
||||
<span>بازگشت به لیست</span>
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -203,16 +203,30 @@ export default function PrescriptionUploadModal({ isOpen, onClose }: Prescriptio
|
||||
type="file"
|
||||
accept="image/*,.pdf"
|
||||
onChange={handleFileChange}
|
||||
className="absolute inset-0 opacity-0 cursor-pointer"
|
||||
className="absolute inset-0 opacity-0 cursor-pointer z-10"
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="w-10 h-10 rounded-full bg-canina-blue/10 flex items-center justify-center text-canina-blue">
|
||||
<Upload className="w-5 h-5" />
|
||||
</div>
|
||||
{file ? (
|
||||
<div className="text-xs font-bold text-canina-blue truncate max-w-xs">{file.name}</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{file.type.startsWith('image/') ? (
|
||||
<img
|
||||
src={URL.createObjectURL(file)}
|
||||
alt="پیشنمایش نسخه"
|
||||
className="w-24 h-24 object-cover rounded-xl border border-medical-gray-200 shadow-sm"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-2xl bg-canina-blue/10 flex items-center justify-center text-canina-blue">
|
||||
<FileText className="w-8 h-8" />
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs font-black text-canina-blue truncate max-w-xs">{file.name}</div>
|
||||
<span className="text-[10px] text-medical-gray-400 font-bold">برای تغییر فایل دوباره کلیک کنید</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-10 h-10 rounded-full bg-canina-blue/10 flex items-center justify-center text-canina-blue">
|
||||
<Upload className="w-5 h-5" />
|
||||
</div>
|
||||
<p className="text-xs font-bold text-medical-gray-700">برای انتخاب فایل یا عکس نسخه کلیک کنید</p>
|
||||
<span className="text-[10px] text-medical-gray-400">فرمتهای مجاز: JPG, PNG, PDF (حداکثر ۱۰ مگابایت)</span>
|
||||
</>
|
||||
@ -334,15 +348,10 @@ export default function PrescriptionUploadModal({ isOpen, onClose }: Prescriptio
|
||||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
className="w-full bg-white border border-medical-gray-200 rounded-xl py-2.5 px-3 text-xs font-mono font-bold text-medical-gray-900 focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
|
||||
className="w-full bg-white border border-medical-gray-200 rounded-xl py-2.5 pr-3 pl-11 text-xs font-mono font-bold text-medical-gray-900 focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
|
||||
/>
|
||||
<Phone className="w-4 h-4 text-medical-gray-400 absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none" />
|
||||
<Phone className="w-4 h-4 text-medical-gray-400 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none" />
|
||||
</div>
|
||||
{isLoggedIn && profile?.mobile && (
|
||||
<span className="text-[10px] text-green-600 font-bold block mt-1">
|
||||
✓ شماره از حساب کاربری شما شناسایی شد.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Additional Notes */}
|
||||
|
||||
@ -242,8 +242,8 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
||||
onClick={() => router.back()}
|
||||
className="flex-1 sm:flex-none px-3.5 py-2 bg-white border border-medical-gray-200 rounded-xl text-medical-gray-600 shadow-xs flex items-center justify-center gap-1.5 font-bold text-xs hover:border-canina-blue hover:text-canina-blue transition-all whitespace-nowrap"
|
||||
>
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
<span>بازگشت</span>
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -681,7 +681,7 @@ export default function ProductPage({ productSlug }: { productSlug: string }) {
|
||||
</div>
|
||||
|
||||
{/* Sticky Sidebar (Clean Buy Box) - Hidden on mobile as mobile floating buy bar is active */}
|
||||
<div className="hidden lg:block lg:col-span-4 lg:sticky lg:top-36 space-y-8">
|
||||
<div className="hidden lg:block lg:col-span-4 lg:sticky lg:top-48 space-y-8">
|
||||
<div className="bg-white rounded-[2.5rem] 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" />
|
||||
|
||||
|
||||
@ -25,13 +25,18 @@ export default function SafeImage({
|
||||
if (!src || error) {
|
||||
return (
|
||||
<div className={cn(
|
||||
"flex flex-col items-center justify-center bg-medical-gray-50/80 border border-medical-gray-100 p-1.5 text-center w-full h-full rounded-2xl relative overflow-hidden select-none",
|
||||
"flex flex-col items-center justify-center bg-medical-gray-50/90 border border-medical-gray-100 p-2 text-center w-full h-full rounded-2xl relative overflow-hidden select-none",
|
||||
className
|
||||
)}>
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-canina-blue rounded-xl flex items-center justify-center text-white font-black text-base sm:text-lg shadow-sm italic shrink-0">
|
||||
C
|
||||
</div>
|
||||
<span className="text-[8px] font-black text-canina-blue/70 font-vazir leading-tight mt-1 line-clamp-1">
|
||||
<Image
|
||||
src="/assets/images/canina-product-placeholder.png"
|
||||
alt={alt}
|
||||
width={240}
|
||||
height={240}
|
||||
className="max-h-[85%] w-auto object-contain opacity-70"
|
||||
unoptimized
|
||||
/>
|
||||
<span className="text-[9px] font-black text-canina-blue/80 font-vazir leading-tight mt-1 line-clamp-1 bg-white/80 px-2 py-0.5 rounded-md shadow-xs">
|
||||
{fallbackText}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -25,6 +25,8 @@ export default function UserDashboard() {
|
||||
const { profile, isLoggedIn, logout, updateProfile, addAddress, updateAddress, deleteAddress, setDefaultAddress, topUpWallet, fetchProfile } = useUserStore();
|
||||
const { pets, activePetId, setActivePet, removePet } = usePetStore();
|
||||
const isWithdrawalEnabled = useSettingsStore((s) => s.getBoolean('wallet_withdrawal_enabled', false));
|
||||
const isCatalogMode = useSettingsStore((s) => s.getBoolean('catalog_mode', false));
|
||||
const isWalletRechargeAllowed = !isCatalogMode && useSettingsStore((s) => s.getBoolean('wallet_recharge_enabled', true));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isLoggedIn && typeof window !== "undefined") {
|
||||
@ -935,14 +937,20 @@ export default function UserDashboard() {
|
||||
برداشت وجه
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsTopUpModalOpen(true)}
|
||||
className="flex-1 sm:flex-initial sm:w-36 h-12 sm:h-14 bg-white text-canina-blue rounded-2xl font-black text-xs sm: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 cursor-pointer whitespace-nowrap"
|
||||
>
|
||||
<ArrowUpCircle className="w-4 h-4 sm:w-5 sm:h-5 group-hover:-translate-y-1 transition-transform shrink-0" />
|
||||
شارژ آنی
|
||||
</button>
|
||||
{isWalletRechargeAllowed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsTopUpModalOpen(true)}
|
||||
className="flex-1 sm:flex-initial sm:w-36 h-12 sm:h-14 bg-white text-canina-blue rounded-2xl font-black text-xs sm: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 cursor-pointer whitespace-nowrap"
|
||||
>
|
||||
<ArrowUpCircle className="w-4 h-4 sm:w-5 sm:h-5 group-hover:-translate-y-1 transition-transform shrink-0" />
|
||||
شارژ آنی
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-[11px] font-bold text-white/80 bg-white/10 px-4 py-3 rounded-2xl border border-white/20 whitespace-nowrap">
|
||||
شارژ در حالت کاتالوگ غیرفعال است
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1203,14 +1211,20 @@ export default function UserDashboard() {
|
||||
<div className="text-2xl sm:text-4xl font-black italic">{toPersian((profile.walletBalance ?? 0).toLocaleString())}</div>
|
||||
<span className="text-xs sm:text-sm not-italic font-bold text-white/80">تومان</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { setActiveTab("wallet"); setIsTopUpModalOpen(true); }}
|
||||
disabled={isTopUpModalOpen}
|
||||
className="mt-5 sm:mt-8 bg-canina-blue text-white w-full py-3.5 sm:py-4 px-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 text-xs sm:text-sm border border-canina-blue cursor-pointer"
|
||||
>
|
||||
<ArrowUpCircle className="w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0" />
|
||||
<span>شارژ کیف پول</span>
|
||||
</button>
|
||||
{isWalletRechargeAllowed ? (
|
||||
<button
|
||||
onClick={() => { setActiveTab("wallet"); setIsTopUpModalOpen(true); }}
|
||||
disabled={isTopUpModalOpen}
|
||||
className="mt-5 sm:mt-8 bg-canina-blue text-white w-full py-3.5 sm:py-4 px-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 text-xs sm:text-sm border border-canina-blue cursor-pointer"
|
||||
>
|
||||
<ArrowUpCircle className="w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0" />
|
||||
<span>شارژ کیف پول</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="mt-5 sm:mt-8 text-[11px] font-bold text-center text-white/70 bg-white/10 py-3 px-2 rounded-2xl border border-white/20">
|
||||
شارژ آنلاین موقتاً غیرفعال است
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { ChevronRight, PlayCircle, ShieldCheck, X, Search, Clock, User } from "lucide-react";
|
||||
import { ChevronRight, ChevronLeft, PlayCircle, ShieldCheck, X, Search, Clock, User } from "lucide-react";
|
||||
import SafeImage from "./SafeImage";
|
||||
|
||||
|
||||
@ -90,10 +90,10 @@ export default function VideosPage() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
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"
|
||||
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 font-vazir"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
بازگشت
|
||||
<span>بازگشت</span>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user