Compare commits
2 Commits
27ef5979aa
...
20a1b225c8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20a1b225c8 | ||
|
|
c125da25ab |
@ -9,6 +9,7 @@ import Link from 'next/link';
|
|||||||
import { toPersian } from "../lib/utils";
|
import { toPersian } from "../lib/utils";
|
||||||
import { useSettingsStore } from "../lib/store/settingsStore";
|
import { useSettingsStore } from "../lib/store/settingsStore";
|
||||||
import api from "../lib/services/api";
|
import api from "../lib/services/api";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function HomeClient() {
|
export default function HomeClient() {
|
||||||
const getText = useSettingsStore(state => state.getText);
|
const getText = useSettingsStore(state => state.getText);
|
||||||
@ -77,10 +78,13 @@ export default function HomeClient() {
|
|||||||
<div className="max-w-7xl mx-auto px-4 grid lg:grid-cols-2 gap-16 lg:gap-24 items-center">
|
<div className="max-w-7xl mx-auto px-4 grid lg:grid-cols-2 gap-16 lg:gap-24 items-center">
|
||||||
<div className="relative overflow-hidden rounded-[3rem]">
|
<div className="relative overflow-hidden rounded-[3rem]">
|
||||||
<div className="absolute -top-10 -right-10 w-40 h-40 bg-canina-blue/5 rounded-full blur-3xl" />
|
<div className="absolute -top-10 -right-10 w-40 h-40 bg-canina-blue/5 rounded-full blur-3xl" />
|
||||||
<img
|
<Image
|
||||||
src="/assets/images/regenerated_image_1779109861747.png"
|
src="/assets/images/regenerated_image_1779109861747.png"
|
||||||
alt="آزمایشگاه تخصصی تولید مکملهای دارویی کانینا آلمان"
|
alt="آزمایشگاه تخصصی تولید مکملهای دارویی کانینا آلمان"
|
||||||
className="rounded-[3rem] shadow-2xl relative z-10 grayscale-[10%] w-full"
|
className="rounded-[3rem] shadow-2xl relative z-10 grayscale-[10%] w-full"
|
||||||
|
width={640}
|
||||||
|
height={425}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<div className="absolute bottom-4 left-4 sm:bottom-6 sm:left-6 bg-canina-blue text-white p-4 sm:p-6 md:p-8 rounded-[1.5rem] sm:rounded-[2rem] shadow-2xl z-20 max-w-[80%] border border-white/10">
|
<div className="absolute bottom-4 left-4 sm:bottom-6 sm:left-6 bg-canina-blue text-white p-4 sm:p-6 md:p-8 rounded-[1.5rem] sm:rounded-[2rem] shadow-2xl z-20 max-w-[80%] border border-white/10">
|
||||||
<div className="text-2xl sm:text-4xl font-black mb-1 font-lalezar tracking-wide">{getText('quality', "کیفیت")}</div>
|
<div className="text-2xl sm:text-4xl font-black mb-1 font-lalezar tracking-wide">{getText('quality', "کیفیت")}</div>
|
||||||
@ -129,7 +133,7 @@ export default function HomeClient() {
|
|||||||
<section className="bg-canina-blue py-12 relative overflow-hidden font-sans">
|
<section className="bg-canina-blue py-12 relative overflow-hidden font-sans">
|
||||||
<div className="absolute inset-0 opacity-10">
|
<div className="absolute inset-0 opacity-10">
|
||||||
<div className="grid grid-cols-10 h-full w-full">
|
<div className="grid grid-cols-10 h-full w-full">
|
||||||
{Array.from({length: 100}).map((_, i) => (
|
{Array.from({ length: 100 }).map((_, i) => (
|
||||||
<div key={i} className="border border-white/20" />
|
<div key={i} className="border border-white/20" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -366,35 +366,37 @@ export default function ArchivePage({
|
|||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
|
|
||||||
{/* Standard Breadcrumb Navigation & Integrated Back Button */}
|
{/* 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">
|
<div className="flex items-center justify-between mb-8 pb-4 border-b border-medical-gray-200/60 font-vazir gap-2">
|
||||||
<div className="flex items-center gap-2 text-xs font-bold text-medical-gray-400 whitespace-nowrap">
|
{/* Back Button - always visible */}
|
||||||
<span className="cursor-pointer hover:text-canina-blue transition-colors" onClick={() => router.push('/')}>خانه</span>
|
|
||||||
<ChevronLeft className="w-3 h-3 text-medical-gray-300" />
|
|
||||||
<span className="cursor-pointer hover:text-canina-blue transition-colors" onClick={() => { setSelectedCategory("all"); }}>کاتالوگ</span>
|
|
||||||
{selectedCategory !== "all" && (
|
|
||||||
<>
|
|
||||||
<ChevronLeft className="w-3 h-3 text-medical-gray-300" />
|
|
||||||
<span className="text-canina-blue font-black">{CATEGORY_LABELS[selectedCategory] || selectedCategory}</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setIsMobileFilterOpen(!isMobileFilterOpen)}
|
|
||||||
className="lg:hidden 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>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push('/')}
|
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"
|
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" />
|
<ChevronRight className="w-3.5 h-3.5" />
|
||||||
<span>بازگشت</span>
|
<span>بازگشت</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Breadcrumb - truncates on mobile */}
|
||||||
|
<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" />
|
||||||
|
<span className="cursor-pointer hover:text-canina-blue transition-colors shrink-0" onClick={() => { setSelectedCategory("all"); }}>کاتالوگ</span>
|
||||||
|
{selectedCategory !== "all" && (
|
||||||
|
<>
|
||||||
|
<ChevronLeft className="w-3 h-3 text-medical-gray-300 shrink-0" />
|
||||||
|
<span className="text-canina-blue font-black truncate">{CATEGORY_LABELS[selectedCategory] || selectedCategory}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col lg:flex-row gap-8">
|
<div className="flex flex-col lg:flex-row gap-8">
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { motion, AnimatePresence } from "motion/react";
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
import { X, User, Building2, Heart, ShieldCheck, Phone, Key, LogIn, ArrowRight, RefreshCw } from "lucide-react";
|
import { X, User, Building2, Heart, ShieldCheck, Phone, Key, LogIn, ArrowRight, RefreshCw, Lock } from "lucide-react";
|
||||||
import { useUserStore, UserRole } from "../lib/store/userStore";
|
import { useUserStore, UserRole } from "../lib/store/userStore";
|
||||||
import { authService } from "../lib/services/authService";
|
import { authService } from "../lib/services/authService";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -14,9 +14,10 @@ interface AuthModalProps {
|
|||||||
|
|
||||||
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
||||||
const { setRole, setLoggedIn, fetchProfile } = useUserStore();
|
const { setRole, setLoggedIn, fetchProfile } = useUserStore();
|
||||||
const [view, setView] = useState<"login" | "register" | "otp-phone" | "otp">("login");
|
const [view, setView] = useState<"login" | "register" | "otp-phone" | "otp" | "forgot-password" | "forgot-otp" | "reset-password">("login");
|
||||||
const [phoneNumber, setPhoneNumber] = useState("");
|
const [phoneNumber, setPhoneNumber] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
const [newPassword, setNewPassword] = useState("");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [firstName, setFirstName] = useState("");
|
const [firstName, setFirstName] = useState("");
|
||||||
const [lastName, setLastName] = useState("");
|
const [lastName, setLastName] = useState("");
|
||||||
@ -40,6 +41,7 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
setView("login");
|
setView("login");
|
||||||
setPhoneNumber("");
|
setPhoneNumber("");
|
||||||
setPassword("");
|
setPassword("");
|
||||||
|
setNewPassword("");
|
||||||
setEmail("");
|
setEmail("");
|
||||||
setFirstName("");
|
setFirstName("");
|
||||||
setLastName("");
|
setLastName("");
|
||||||
@ -159,6 +161,67 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleForgotSendOtp = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const cleanPhone = phoneNumber.trim();
|
||||||
|
if (!/^09\d{9}$/.test(cleanPhone)) {
|
||||||
|
toast.error("شماره موبایل نامعتبر است");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await authService.sendOtp(cleanPhone);
|
||||||
|
if ((res as any).code) {
|
||||||
|
toast.info(`کد تایید (تست): ${(res as any).code}`, { duration: 10000 });
|
||||||
|
} else {
|
||||||
|
toast.success("کد بازیابی رمز پیامک شد");
|
||||||
|
}
|
||||||
|
setView("forgot-otp");
|
||||||
|
setCountdown(120);
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error(err.message || "خطا در ارسال کد");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleForgotVerifyOtp = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (otpCode.trim().length !== 5) {
|
||||||
|
toast.error("کد باید ۵ رقم باشد");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Verify OTP - if success, allow password reset
|
||||||
|
await authService.verifyOtp(phoneNumber.trim(), otpCode.trim());
|
||||||
|
setView("reset-password");
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error(err.message || "کد تایید نامعتبر است");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetPassword = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
toast.error("رمز جدید باید حداقل ۶ کاراکتر باشد");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Use authService to update password after OTP verification
|
||||||
|
await authService.updateProfile({ password: newPassword } as any);
|
||||||
|
toast.success("رمز عبور با موفقیت تغییر یافت");
|
||||||
|
setView("login");
|
||||||
|
} catch (err: any) {
|
||||||
|
toast.error(err.message || "خطا در تغییر رمز");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
@ -219,10 +282,13 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-bold text-medical-gray-500">شماره موبایل</label>
|
<label htmlFor="login-phone" className="text-xs font-bold text-medical-gray-500">شماره موبایل</label>
|
||||||
<input
|
<input
|
||||||
|
id="login-phone"
|
||||||
|
name="username"
|
||||||
type="tel"
|
type="tel"
|
||||||
maxLength={11}
|
maxLength={11}
|
||||||
|
autoComplete="username"
|
||||||
placeholder="مثال: ۰۹۱۲۳۴۵۶۷۸۹"
|
placeholder="مثال: ۰۹۱۲۳۴۵۶۷۸۹"
|
||||||
value={phoneNumber}
|
value={phoneNumber}
|
||||||
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ''))}
|
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ''))}
|
||||||
@ -231,9 +297,12 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-bold text-medical-gray-500">رمز عبور</label>
|
<label htmlFor="login-password" className="text-xs font-bold text-medical-gray-500">رمز عبور</label>
|
||||||
<input
|
<input
|
||||||
|
id="login-password"
|
||||||
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
placeholder="رمز عبور خود را وارد کنید"
|
placeholder="رمز عبور خود را وارد کنید"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
@ -242,7 +311,7 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between pt-2">
|
<div className="flex items-center justify-between pt-2 flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setView("otp-phone")}
|
onClick={() => setView("otp-phone")}
|
||||||
@ -250,6 +319,14 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
>
|
>
|
||||||
ورود با کد یکبار مصرف (OTP)
|
ورود با کد یکبار مصرف (OTP)
|
||||||
</button>
|
</button>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setPhoneNumber(""); setView("forgot-password"); }}
|
||||||
|
className="text-xs font-bold text-medical-gray-400 hover:text-canina-blue hover:underline"
|
||||||
|
>
|
||||||
|
فراموشی رمز
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setView("register")}
|
onClick={() => setView("register")}
|
||||||
@ -258,6 +335,7 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
حساب ندارید؟ ثبتنام
|
حساب ندارید؟ ثبتنام
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -280,9 +358,12 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
>
|
>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-xs font-bold text-medical-gray-500">نام</label>
|
<label htmlFor="reg-fname" className="text-xs font-bold text-medical-gray-500">نام</label>
|
||||||
<input
|
<input
|
||||||
|
id="reg-fname"
|
||||||
|
name="given-name"
|
||||||
type="text"
|
type="text"
|
||||||
|
autoComplete="given-name"
|
||||||
value={firstName}
|
value={firstName}
|
||||||
onChange={(e) => setFirstName(e.target.value)}
|
onChange={(e) => setFirstName(e.target.value)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -290,9 +371,12 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-xs font-bold text-medical-gray-500">نام خانوادگی</label>
|
<label htmlFor="reg-lname" className="text-xs font-bold text-medical-gray-500">نام خانوادگی</label>
|
||||||
<input
|
<input
|
||||||
|
id="reg-lname"
|
||||||
|
name="family-name"
|
||||||
type="text"
|
type="text"
|
||||||
|
autoComplete="family-name"
|
||||||
value={lastName}
|
value={lastName}
|
||||||
onChange={(e) => setLastName(e.target.value)}
|
onChange={(e) => setLastName(e.target.value)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -302,10 +386,13 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-xs font-bold text-medical-gray-500">شماره موبایل *</label>
|
<label htmlFor="reg-phone" className="text-xs font-bold text-medical-gray-500">شماره موبایل *</label>
|
||||||
<input
|
<input
|
||||||
|
id="reg-phone"
|
||||||
|
name="username"
|
||||||
type="tel"
|
type="tel"
|
||||||
maxLength={11}
|
maxLength={11}
|
||||||
|
autoComplete="username"
|
||||||
value={phoneNumber}
|
value={phoneNumber}
|
||||||
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ''))}
|
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ''))}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -314,9 +401,12 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-xs font-bold text-medical-gray-500">ایمیل (اختیاری)</label>
|
<label htmlFor="reg-email" className="text-xs font-bold text-medical-gray-500">ایمیل (اختیاری)</label>
|
||||||
<input
|
<input
|
||||||
|
id="reg-email"
|
||||||
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -325,9 +415,12 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label className="text-xs font-bold text-medical-gray-500">رمز عبور *</label>
|
<label htmlFor="reg-password" className="text-xs font-bold text-medical-gray-500">رمز عبور *</label>
|
||||||
<input
|
<input
|
||||||
|
id="reg-password"
|
||||||
|
name="new-password"
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -477,6 +570,119 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) {
|
|||||||
</button>
|
</button>
|
||||||
</motion.form>
|
</motion.form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Forgot Password - Enter Phone */}
|
||||||
|
{view === "forgot-password" && (
|
||||||
|
<motion.form
|
||||||
|
key="forgot-password-view"
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
onSubmit={handleForgotSendOtp}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-2xl p-4 text-center">
|
||||||
|
<Lock className="w-8 h-8 text-amber-500 mx-auto mb-2" />
|
||||||
|
<p className="text-sm font-bold text-amber-700">برای بازیابی رمز عبور، کد تایید به شمارهتان ارسال میشود</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="forgot-phone" className="text-xs font-bold text-medical-gray-500">شماره موبایل</label>
|
||||||
|
<input
|
||||||
|
id="forgot-phone"
|
||||||
|
name="username"
|
||||||
|
type="tel"
|
||||||
|
maxLength={11}
|
||||||
|
autoComplete="username"
|
||||||
|
placeholder="مثال: ۰۹۱۲۳۴۵۶۷۸۹"
|
||||||
|
value={phoneNumber}
|
||||||
|
onChange={(e) => setPhoneNumber(e.target.value.replace(/[^0-9]/g, ''))}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 px-4 focus:ring-2 focus:ring-canina-blue/20 outline-none font-bold text-left tracking-widest"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || phoneNumber.length < 11}
|
||||||
|
className="w-full py-4 bg-canina-blue text-white rounded-xl font-black hover:bg-indigo-700 disabled:opacity-50 transition-all shadow-lg shadow-canina-blue/20 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{isLoading ? <RefreshCw className="w-5 h-5 animate-spin" /> : <>ارسال کد بازیابی</>}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => setView("login")} className="w-full text-xs font-bold text-medical-gray-400 hover:text-canina-blue">بازگشت به ورود</button>
|
||||||
|
</motion.form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Forgot Password - Enter OTP */}
|
||||||
|
{view === "forgot-otp" && (
|
||||||
|
<motion.form
|
||||||
|
key="forgot-otp-view"
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
onSubmit={handleForgotVerifyOtp}
|
||||||
|
className="space-y-5"
|
||||||
|
>
|
||||||
|
<p className="text-sm font-bold text-medical-gray-600 text-center">کد ارسال شده به {toPersian(phoneNumber)} را وارد کنید</p>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
ref={otpInputRef}
|
||||||
|
id="forgot-otp-code"
|
||||||
|
name="one-time-code"
|
||||||
|
type="text"
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
inputMode="numeric"
|
||||||
|
maxLength={5}
|
||||||
|
value={otpCode}
|
||||||
|
onChange={(e) => setOtpCode(e.target.value.replace(/[^0-9]/g, ''))}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-4 pl-4 pr-4 focus:ring-2 focus:ring-canina-blue/20 outline-none font-black text-2xl text-center tracking-[0.5em]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{countdown > 0 ? (
|
||||||
|
<p className="text-xs font-bold text-medical-gray-400 text-center">ارسال مجدد پس از {toPersian(countdown)} ثانیه</p>
|
||||||
|
) : (
|
||||||
|
<button type="button" onClick={handleForgotSendOtp as any} disabled={isLoading} className="text-xs font-black text-canina-blue hover:underline flex items-center gap-1.5 justify-center mx-auto">
|
||||||
|
<RefreshCw className="w-3.5 h-3.5" />ارسال مجدد
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button type="submit" disabled={isLoading || otpCode.length < 5} className="w-full py-4 bg-canina-blue text-white rounded-xl font-black disabled:opacity-50 transition-all shadow-lg shadow-canina-blue/20 flex items-center justify-center gap-2">
|
||||||
|
{isLoading ? <RefreshCw className="w-5 h-5 animate-spin" /> : <>تایید کد</>}
|
||||||
|
</button>
|
||||||
|
</motion.form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Reset Password */}
|
||||||
|
{view === "reset-password" && (
|
||||||
|
<motion.form
|
||||||
|
key="reset-password-view"
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
onSubmit={handleResetPassword}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-2xl p-4 text-center">
|
||||||
|
<p className="text-sm font-bold text-green-700">هویت تایید شد. رمز جدید خود را وارد کنید</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="new-password" className="text-xs font-bold text-medical-gray-500">رمز جدید</label>
|
||||||
|
<input
|
||||||
|
id="new-password"
|
||||||
|
name="new-password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
placeholder="حداقل ۶ کاراکتر"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 px-4 focus:ring-2 focus:ring-canina-blue/20 outline-none text-left"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" disabled={isLoading || newPassword.length < 6} className="w-full py-4 bg-canina-blue text-white rounded-xl font-black disabled:opacity-50 transition-all shadow-lg shadow-canina-blue/20 flex items-center justify-center gap-2">
|
||||||
|
{isLoading ? <RefreshCw className="w-5 h-5 animate-spin" /> : <>تغییر رمز عبور</>}
|
||||||
|
</button>
|
||||||
|
</motion.form>
|
||||||
|
)}
|
||||||
|
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -36,9 +36,27 @@ export default function CheckoutPage() {
|
|||||||
const [dbProducts, setDbProducts] = useState<Product[]>([]);
|
const [dbProducts, setDbProducts] = useState<Product[]>([]);
|
||||||
const [paymentMethod, setPaymentMethod] = useState<string>('online');
|
const [paymentMethod, setPaymentMethod] = useState<string>('online');
|
||||||
|
|
||||||
|
const [selectedAddressId, setSelectedAddressId] = useState<string>('');
|
||||||
|
const [showNewAddressForm, setShowNewAddressForm] = useState(false);
|
||||||
|
|
||||||
|
// Custom manual address state if guest or not using stored addresses
|
||||||
|
const [manualName, setManualName] = useState('');
|
||||||
|
const [manualPhone, setManualPhone] = useState('');
|
||||||
|
const [manualAddress, setManualAddress] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
productService.getProducts({ limit: 999 }).then(res => setDbProducts(res.data));
|
productService.getProducts({ limit: 999 }).then(res => setDbProducts(res.data));
|
||||||
}, []);
|
|
||||||
|
// Set default selected address if logged in
|
||||||
|
if (profile.addresses && profile.addresses.length > 0) {
|
||||||
|
const def = profile.addresses.find(a => a.isDefault) || profile.addresses[0];
|
||||||
|
setSelectedAddressId(def.id);
|
||||||
|
} else if (profile.firstName) {
|
||||||
|
// Pre-fill profile info for manual fields
|
||||||
|
setManualName(`${profile.firstName} ${profile.lastName}`.trim());
|
||||||
|
setManualPhone(profile.mobile || '');
|
||||||
|
}
|
||||||
|
}, [profile]);
|
||||||
|
|
||||||
const subtotal = getSubtotal();
|
const subtotal = getSubtotal();
|
||||||
const roundedAmount = Math.ceil(subtotal / 10000) * 10000;
|
const roundedAmount = Math.ceil(subtotal / 10000) * 10000;
|
||||||
@ -55,26 +73,49 @@ export default function CheckoutPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFinalize = async () => {
|
const handleFinalize = async () => {
|
||||||
|
const totalAmount = getTotal();
|
||||||
|
|
||||||
|
if (paymentMethod === 'wallet' && profile.walletBalance < totalAmount) {
|
||||||
|
toast.error("اعتبار کیف پول شما کافی نیست. لطفاً ابتدا آن را شارژ کنید یا روش پرداخت دیگری انتخاب کنید.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const activePet = getActivePet();
|
const activePet = getActivePet();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Register the order in the backend
|
// 1. If using wallet, deduct balance and add purchase transaction
|
||||||
|
if (paymentMethod === 'wallet') {
|
||||||
|
const newBalance = profile.walletBalance - totalAmount;
|
||||||
|
const newTransaction = {
|
||||||
|
id: `TR-${Math.floor(Math.random() * 90000) + 10000}`,
|
||||||
|
type: 'purchase' as const,
|
||||||
|
amount: totalAmount,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
status: 'success' as const
|
||||||
|
};
|
||||||
|
await updateProfile({
|
||||||
|
walletBalance: newBalance,
|
||||||
|
transactions: [newTransaction, ...(profile.transactions || [])]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Register the order in the backend
|
||||||
const orderId = await addOrder({
|
const orderId = await addOrder({
|
||||||
items: [...items],
|
items: [...items],
|
||||||
total: getTotal(),
|
total: totalAmount,
|
||||||
charityDonation: charityDonation,
|
charityDonation: charityDonation,
|
||||||
petId: activePet?.id
|
petId: activePet?.id
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Update user charity total
|
// 3. Update user charity total
|
||||||
if (charityDonation > 0) {
|
if (charityDonation > 0) {
|
||||||
updateProfile({
|
await updateProfile({
|
||||||
charityDonationTotal: (profile.charityDonationTotal || 0) + charityDonation
|
charityDonationTotal: (profile.charityDonationTotal || 0) + charityDonation
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Update pet consumptions for the refill logic
|
// 4. Update pet consumptions for the refill logic
|
||||||
if (activePet) {
|
if (activePet) {
|
||||||
const newConsumptions = [...(activePet.consumptions || [])];
|
const newConsumptions = [...(activePet.consumptions || [])];
|
||||||
|
|
||||||
@ -158,20 +199,99 @@ export default function CheckoutPage() {
|
|||||||
<h2 className="text-xl sm:text-2xl font-black text-medical-gray-900 italic">اطلاعات ارسال</h2>
|
<h2 className="text-xl sm:text-2xl font-black text-medical-gray-900 italic">اطلاعات ارسال</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{profile.addresses && profile.addresses.length > 0 && !showNewAddressForm ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-xs font-bold text-medical-gray-500 mb-2">انتخاب از بین آدرسهای ذخیرهشده شما:</p>
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{profile.addresses.map((addr) => (
|
||||||
|
<div
|
||||||
|
key={addr.id}
|
||||||
|
onClick={() => setSelectedAddressId(addr.id)}
|
||||||
|
className={cn(
|
||||||
|
"p-4 rounded-2xl border-2 cursor-pointer transition-all flex items-start gap-3",
|
||||||
|
selectedAddressId === addr.id ? "border-canina-blue bg-canina-blue/5" : "border-medical-gray-100 hover:border-medical-gray-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="selectedAddress"
|
||||||
|
checked={selectedAddressId === addr.id}
|
||||||
|
onChange={() => setSelectedAddressId(addr.id)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="font-black text-sm text-medical-gray-900">{addr.title}</span>
|
||||||
|
{addr.isDefault && (
|
||||||
|
<span className="bg-canina-blue/10 text-canina-blue text-[9px] font-black px-2 py-0.5 rounded-full">پیشفرض</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-medical-gray-600 leading-relaxed truncate">{addr.province}، {addr.city}، {addr.detail}</p>
|
||||||
|
<p className="text-[10px] text-medical-gray-400 font-bold mt-1">گیرنده: {addr.receptorName} | همراه: {toPersian(addr.phone)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewAddressForm(true)}
|
||||||
|
className="mt-4 flex items-center gap-2 text-canina-blue font-black text-xs hover:underline"
|
||||||
|
>
|
||||||
|
<PlusCircle className="w-4 h-4" />
|
||||||
|
<span>استفاده از آدرس جدید</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{profile.addresses && profile.addresses.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewAddressForm(false)}
|
||||||
|
className="mb-4 text-xs font-bold text-canina-blue hover:underline"
|
||||||
|
>
|
||||||
|
← بازگشت به آدرسهای ذخیرهشده
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div className="grid md:grid-cols-2 gap-4 sm:gap-6">
|
<div className="grid md:grid-cols-2 gap-4 sm:gap-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">نام و نام خانوادگی</label>
|
<label htmlFor="chk-fullname" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">نام و نام خانوادگی</label>
|
||||||
<input type="text" className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20" placeholder="مثلاً: پارسا آقایی" />
|
<input
|
||||||
|
id="chk-fullname"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
autoComplete="name"
|
||||||
|
value={manualName}
|
||||||
|
onChange={(e) => setManualName(e.target.value)}
|
||||||
|
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
|
||||||
|
placeholder="مثلاً: پارسا آقایی"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">شماره تماس</label>
|
<label htmlFor="chk-tel" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">شماره تماس</label>
|
||||||
<input type="text" className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20" placeholder="۰۹۱۲XXXXXXX" />
|
<input
|
||||||
|
id="chk-tel"
|
||||||
|
name="tel"
|
||||||
|
type="tel"
|
||||||
|
autoComplete="tel"
|
||||||
|
value={manualPhone}
|
||||||
|
onChange={(e) => setManualPhone(e.target.value.replace(/[^0-9]/g, ''))}
|
||||||
|
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20"
|
||||||
|
placeholder="۰۹۱۲XXXXXXX"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2 space-y-2">
|
<div className="md:col-span-2 space-y-2">
|
||||||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">آدرس دقیق پستی</label>
|
<label htmlFor="chk-street" className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block">آدرس دقیق پستی</label>
|
||||||
<textarea className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20 h-28 sm:h-32" placeholder="استان، شهر، خیابان..." />
|
<textarea
|
||||||
|
id="chk-street"
|
||||||
|
name="street-address"
|
||||||
|
autoComplete="street-address"
|
||||||
|
value={manualAddress}
|
||||||
|
onChange={(e) => setManualAddress(e.target.value)}
|
||||||
|
className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-2xl py-3 sm:py-4 px-4 sm:px-6 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-canina-blue/20 h-28 sm:h-32"
|
||||||
|
placeholder="استان، شهر، خیابان..."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Step 2: Payment */}
|
{/* Step 2: Payment */}
|
||||||
@ -258,46 +378,46 @@ export default function CheckoutPage() {
|
|||||||
{/* Sidebar Summary */}
|
{/* Sidebar Summary */}
|
||||||
<div className="lg:col-span-4 space-y-8">
|
<div className="lg:col-span-4 space-y-8">
|
||||||
<div className="bg-medical-gray-900 rounded-[3rem] p-10 text-white shadow-2xl sticky top-32">
|
<div className="bg-medical-gray-900 rounded-[3rem] p-10 text-white shadow-2xl sticky top-32">
|
||||||
<h3 className="text-2xl font-black italic mb-8 border-b border-white/10 pb-4">خلاصه سفارش</h3>
|
<h3 className="text-2xl font-black italic mb-8 border-b border-white/20 pb-4 text-white">خلاصه سفارش</h3>
|
||||||
|
|
||||||
<div className="space-y-6 mb-10 overflow-y-auto max-h-60 pr-2">
|
<div className="space-y-6 mb-10 overflow-y-auto max-h-60 pr-2">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<div key={item.product.id} className="flex justify-between items-start gap-4">
|
<div key={item.product.id} className="flex justify-between items-start gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-black text-white/90 leading-tight">{item.product.name}</p>
|
<p className="text-sm font-black text-white leading-tight">{item.product.name}</p>
|
||||||
<p className="text-[10px] text-white/40 font-bold mt-1">تعداد: {toPersian(item.quantity)} بسته</p>
|
<p className="text-[10px] text-white/60 font-bold mt-1">تعداد: {toPersian(item.quantity)} بسته</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-mono font-bold font-vazir">{toPersian((item.product.priceValue * item.quantity).toLocaleString())}</span>
|
<span className="text-xs font-mono font-bold font-vazir text-white">{toPersian((item.product.priceValue * item.quantity).toLocaleString())} تومان</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 pt-8 border-t border-white/10">
|
<div className="space-y-4 pt-8 border-t border-white/20">
|
||||||
<div className="flex justify-between text-sm font-bold text-white/60">
|
<div className="flex justify-between text-sm font-bold text-white/80">
|
||||||
<span>مجموع اقلام</span>
|
<span>مجموع اقلام</span>
|
||||||
<span className="font-vazir">{toPersian(getSubtotal().toLocaleString())} تومان</span>
|
<span className="font-vazir text-white">{toPersian(getSubtotal().toLocaleString())} تومان</span>
|
||||||
</div>
|
</div>
|
||||||
{isSubscribed && (
|
{isSubscribed && (
|
||||||
<div className="flex justify-between text-sm font-bold text-green-400">
|
<div className="flex justify-between text-sm font-bold text-green-400">
|
||||||
<span>تخفیف اشتراک (۵٪-)</span>
|
<span>تخفیف اشتراک (۵٪-)</span>
|
||||||
<span className="font-vazir">{toPersian(getDiscount().toLocaleString())} تومان</span>
|
<span className="font-vazir text-green-400">{toPersian(getDiscount().toLocaleString())} تومان</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{charityDonation > 0 && (
|
{charityDonation > 0 && (
|
||||||
<div className="flex justify-between text-sm font-bold text-pink-400">
|
<div className="flex justify-between text-sm font-bold text-pink-400">
|
||||||
<span>ردپای مهربانی</span>
|
<span>ردپای مهربانی</span>
|
||||||
<span className="font-vazir">{toPersian(charityDonation.toLocaleString())} تومان+</span>
|
<span className="font-vazir text-pink-400">{toPersian(charityDonation.toLocaleString())} تومان+</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-between text-sm font-bold text-white/60">
|
<div className="flex justify-between text-sm font-bold text-white/80">
|
||||||
<span>هزینه ارسال</span>
|
<span>هزینه ارسال</span>
|
||||||
<span className="text-green-400 uppercase tracking-widest text-[10px] font-black">رایگان</span>
|
<span className="text-green-400 uppercase tracking-widest text-[10px] font-black">رایگان</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-end pt-4">
|
<div className="flex justify-between items-end pt-4">
|
||||||
<span className="text-xl font-black italic">مبلغ نهایی</span>
|
<span className="text-xl font-black italic text-white">مبلغ نهایی</span>
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<p className="text-3xl font-black text-canina-blue leading-none font-vazir">{toPersian(getTotal().toLocaleString())}</p>
|
<p className="text-3xl font-black text-canina-gold leading-none font-vazir">{toPersian(getTotal().toLocaleString())}</p>
|
||||||
<p className="text-[10px] text-white/30 font-bold mt-1">تومان</p>
|
<p className="text-[10px] text-white/50 font-bold mt-1">تومان</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -318,8 +438,10 @@ export default function CheckoutPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
id="chk-coupon"
|
||||||
name="coupon"
|
name="coupon"
|
||||||
type="text"
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
placeholder="کد تخفیف (مثلاً: CANINO2024)"
|
placeholder="کد تخفیف (مثلاً: CANINO2024)"
|
||||||
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs focus:ring-1 focus:ring-canina-blue outline-none"
|
className="flex-1 bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-xs focus:ring-1 focus:ring-canina-blue outline-none"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -300,39 +300,39 @@ export default function Header({
|
|||||||
|
|
||||||
<div className="flex gap-1.5 sm:gap-2 items-center h-12">
|
<div className="flex gap-1.5 sm:gap-2 items-center h-12">
|
||||||
{isLoggedIn ? (
|
{isLoggedIn ? (
|
||||||
<div className="flex items-center h-full gap-1.5 sm:gap-2 p-1 bg-medical-gray-50/50 border border-medical-gray-100 rounded-3xl">
|
<div className="flex items-center h-full gap-1 sm:gap-2 p-1 bg-medical-gray-50/50 border border-medical-gray-100 rounded-3xl">
|
||||||
{/* User Name (Direct link to dashboard) */}
|
{/* User Icon/Name (Direct link to dashboard) */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-1.5 sm:gap-2.5 px-2.5 sm:px-4 py-2 hover:bg-white rounded-2xl cursor-pointer transition-all border border-transparent hover:border-medical-gray-200"
|
className="flex items-center gap-1 sm:gap-2.5 px-2 sm:px-4 py-2 hover:bg-white rounded-2xl cursor-pointer transition-all border border-transparent hover:border-medical-gray-200 min-w-0"
|
||||||
onClick={() => onNavigate("user-dashboard")}
|
onClick={() => onNavigate("user-dashboard")}
|
||||||
>
|
>
|
||||||
<User className="w-4 h-4 sm:w-5 sm:h-5 text-canina-blue" />
|
<User className="w-4 h-4 sm:w-5 sm:h-5 text-canina-blue flex-shrink-0" />
|
||||||
<span className="text-[12px] sm:text-[14px] font-black font-vazir text-medical-gray-900 leading-none">
|
<span className="hidden sm:inline text-[14px] font-black font-vazir text-medical-gray-900 leading-none truncate max-w-[80px]">
|
||||||
{profile.firstName}
|
{profile.firstName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Separator Line */}
|
{/* Separator Line */}
|
||||||
<div className="w-[1.5px] h-4 sm:h-5 bg-medical-gray-200" />
|
<div className="w-[1.5px] h-4 sm:h-5 bg-medical-gray-200 flex-shrink-0" />
|
||||||
|
|
||||||
{/* Pet Name & Switcher */}
|
{/* Pet Icon & Switcher */}
|
||||||
<div className="relative h-full flex items-center" ref={petMenuRef}>
|
<div className="relative h-full flex items-center" ref={petMenuRef}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-1.5 sm:gap-2.5 px-2.5 sm:px-4 py-2 rounded-2xl transition-all cursor-pointer",
|
"flex items-center gap-1 sm:gap-2.5 px-2 sm:px-4 py-2 rounded-2xl transition-all cursor-pointer min-w-0",
|
||||||
isPetSwitcherOpen ? "bg-canina-blue text-white shadow-lg shadow-canina-blue/20" : "text-canina-blue hover:bg-white"
|
isPetSwitcherOpen ? "bg-canina-blue text-white shadow-lg shadow-canina-blue/20" : "text-canina-blue hover:bg-white"
|
||||||
)}
|
)}
|
||||||
onClick={() => setIsPetSwitcherOpen(!isPetSwitcherOpen)}
|
onClick={() => setIsPetSwitcherOpen(!isPetSwitcherOpen)}
|
||||||
>
|
>
|
||||||
{activePet?.type === "گربه" ? (
|
{activePet?.type === "گربه" ? (
|
||||||
<Cat className={cn("w-4 h-4 sm:w-5 sm:h-5", isPetSwitcherOpen ? "text-white" : "text-canina-blue/60")} />
|
<Cat className={cn("w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0", isPetSwitcherOpen ? "text-white" : "text-canina-blue/60")} />
|
||||||
) : (
|
) : (
|
||||||
<Dog className={cn("w-4 h-4 sm:w-5 sm:h-5", isPetSwitcherOpen ? "text-white" : "text-canina-blue/60")} />
|
<Dog className={cn("w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0", isPetSwitcherOpen ? "text-white" : "text-canina-blue/60")} />
|
||||||
)}
|
)}
|
||||||
<span className={cn("text-[12px] sm:text-[14px] font-black font-vazir leading-none", isPetSwitcherOpen ? "text-white" : "text-canina-blue")}>
|
<span className={cn("hidden sm:inline text-[14px] font-black font-vazir leading-none truncate max-w-[80px]", isPetSwitcherOpen ? "text-white" : "text-canina-blue")}>
|
||||||
{activePet?.name || "بدون پت"}
|
{activePet?.name || "بدون پت"}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className={cn("w-3.5 h-3.5 sm:w-4 sm:h-4 transition-transform", isPetSwitcherOpen ? "rotate-180" : "")} />
|
<ChevronDown className={cn("w-3 h-3 sm:w-4 sm:h-4 transition-transform flex-shrink-0", isPetSwitcherOpen ? "rotate-180" : "")} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user