- Fix seed-products.ts TS error (implicit any) and BOM handling - Re-run full seed to restore correct Persian encoding in DB - Fix productService query→search param mismatch - Fix IngredientWiki hardcoded port 4000→use settingsStore - Restore seed-products-data.json from git after accidental corruption
156 lines
6.8 KiB
TypeScript
156 lines
6.8 KiB
TypeScript
"use client";
|
||
import React, { useState } from "react";
|
||
import { motion, AnimatePresence } from "motion/react";
|
||
import { X, CreditCard, Sparkles, CheckCircle2, TrendingUp, DollarSign } from "lucide-react";
|
||
import { toPersian, cn } from "../lib/utils";
|
||
import { toast } from "sonner";
|
||
|
||
interface TopUpModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
onConfirm: (amount: number) => void;
|
||
}
|
||
|
||
const PRESET_AMOUNTS = [
|
||
{ label: "۵۰۰,۰۰۰ تومان", value: 500000 },
|
||
{ label: "۱,۰۰۰,۰۰۰ تومان", value: 1000000 },
|
||
{ label: "۲,۰۰۰,۰۰۰ تومان", value: 2000000 },
|
||
];
|
||
|
||
export default function TopUpModal({ isOpen, onClose, onConfirm }: TopUpModalProps) {
|
||
const [amount, setAmount] = useState<string>("");
|
||
const [selectedPreset, setSelectedPreset] = useState<number | null>(null);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
const handlePresetSelect = (val: number) => {
|
||
setSelectedPreset(val);
|
||
setAmount(val.toString());
|
||
};
|
||
|
||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const val = e.target.value.replace(/,/g, '');
|
||
if (/^\d*$/.test(val)) {
|
||
setAmount(val);
|
||
setSelectedPreset(null);
|
||
}
|
||
};
|
||
|
||
const handleSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
const finalAmount = parseInt(amount);
|
||
if (isNaN(finalAmount) || finalAmount < 10000) {
|
||
toast.error("حداقل مبلغ شارژ ۱۰,۰۰۰ تومان است");
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
// Simulate gateway connection
|
||
setTimeout(() => {
|
||
onConfirm(finalAmount);
|
||
toast.success(`مبلغ ${toPersian(finalAmount.toLocaleString())} تومان به کیف پول شما اضافه شد.`);
|
||
setLoading(false);
|
||
onClose();
|
||
setAmount("");
|
||
setSelectedPreset(null);
|
||
}, 2000);
|
||
};
|
||
|
||
return (
|
||
<AnimatePresence>
|
||
{isOpen && (
|
||
<div className="fixed inset-0 z-[110] flex items-center justify-center p-4">
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
onClick={onClose}
|
||
className="absolute inset-0 bg-medical-gray-900/60 backdrop-blur-md"
|
||
/>
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.9, y: 20 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||
className="bg-white w-full max-w-md rounded-[3rem] relative z-10 shadow-2xl overflow-hidden font-vazir text-right"
|
||
dir="rtl"
|
||
>
|
||
<div className="bg-medical-gray-50 px-8 py-6 border-b border-medical-gray-100 flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-canina-blue rounded-xl flex items-center justify-center text-white">
|
||
<TrendingUp className="w-5 h-5" />
|
||
</div>
|
||
<h3 className="text-xl font-black text-medical-gray-900 italic">شارژ سریع کیف پول</h3>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="w-10 h-10 flex items-center justify-center rounded-xl bg-white border border-medical-gray-200 text-medical-gray-400 hover:text-red-500 transition-all"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="p-8 space-y-8">
|
||
<div className="space-y-4">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">یکی از مبالغ پیشنهادی را انتخاب کنید</label>
|
||
<div className="flex flex-wrap gap-2">
|
||
{PRESET_AMOUNTS.map((preset) => (
|
||
<button
|
||
key={preset.value}
|
||
type="button"
|
||
onClick={() => handlePresetSelect(preset.value)}
|
||
className={cn(
|
||
"px-6 py-3 rounded-2xl text-sm font-black transition-all border",
|
||
selectedPreset === preset.value
|
||
? "bg-canina-blue text-white border-canina-blue shadow-lg shadow-canina-blue/20"
|
||
: "bg-white text-medical-gray-700 border-medical-gray-100 hover:border-canina-blue"
|
||
)}
|
||
>
|
||
{preset.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<label className="text-[10px] font-black text-medical-gray-400 uppercase tracking-widest block pr-2">یا مبلغ دلخواه خود را وارد کنید (تومان)</label>
|
||
<div className="relative">
|
||
<input
|
||
autoFocus
|
||
type="text"
|
||
value={amount ? parseInt(amount).toLocaleString() : ""}
|
||
onChange={handleInputChange}
|
||
className="w-full bg-medical-gray-50 border border-medical-gray-100 rounded-3xl py-6 px-8 outline-none font-black text-2xl text-center text-medical-gray-900 transition-all focus:ring-4 focus:ring-canina-blue/10 focus:border-canina-blue italic"
|
||
placeholder="۰"
|
||
/>
|
||
{amount && (
|
||
<div className="absolute left-6 top-1/2 -translate-y-1/2 text-xs font-bold text-medical-gray-400">تومان</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-blue-50 border border-blue-100 rounded-2xl p-4 flex gap-3">
|
||
<Sparkles className="w-5 h-5 text-canina-blue shrink-0" />
|
||
<p className="text-[10px] font-bold text-canina-blue leading-relaxed">با شارژ کیف پول، از هدیههای هوشمند کانینا و اولویت در ارسال سفارشها بهرهمند شوید.</p>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={loading || !amount}
|
||
className="w-full py-5 bg-medical-gray-900 text-white rounded-[2rem] font-black text-lg flex items-center justify-center gap-3 hover:bg-canina-blue transition-all shadow-xl shadow-black/10 disabled:opacity-50 disabled:cursor-not-allowed group"
|
||
>
|
||
{loading ? (
|
||
<div className="w-6 h-6 border-4 border-white/20 border-t-white rounded-full animate-spin" />
|
||
) : (
|
||
<>
|
||
<CreditCard className="w-6 h-6 group-hover:scale-110 transition-transform" />
|
||
اتصال به درگاه بانکی
|
||
</>
|
||
)}
|
||
</button>
|
||
</form>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
</AnimatePresence>
|
||
);
|
||
}
|