canina/frontend/application/components/TopUpModal.tsx
parsa aghaei 7615aa0e51 fix: resolve seed encoding, search param, and wiki data source issues
- 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
2026-07-11 15:40:49 +03:30

156 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

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

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

"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>
);
}