feat: add document upload state and improve responsive design of investor profile page
This commit is contained in:
@@ -50,6 +50,8 @@ export default function InvestorProfilePage() {
|
|||||||
const [showPassword, setShowPassword] = useState({ current: false, new: false, confirm: false });
|
const [showPassword, setShowPassword] = useState({ current: false, new: false, confirm: false });
|
||||||
const [passwordForm, setPasswordForm] = useState({ current: '', new: '', confirm: '' });
|
const [passwordForm, setPasswordForm] = useState({ current: '', new: '', confirm: '' });
|
||||||
const [twoFactorEnabled, setTwoFactorEnabled] = useState(false);
|
const [twoFactorEnabled, setTwoFactorEnabled] = useState(false);
|
||||||
|
const [showUploadDocModal, setShowUploadDocModal] = useState(false);
|
||||||
|
const [uploadDocForm, setUploadDocForm] = useState({ docType: '', docNumber: '', docFile: null as File | null });
|
||||||
|
|
||||||
const [showAddBankModal, setShowAddBankModal] = useState(false);
|
const [showAddBankModal, setShowAddBankModal] = useState(false);
|
||||||
const [showEditBankModal, setShowEditBankModal] = useState(false);
|
const [showEditBankModal, setShowEditBankModal] = useState(false);
|
||||||
@@ -122,36 +124,36 @@ export default function InvestorProfilePage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 lg:p-6 bg-slate-50 min-h-screen">
|
<div className="p-4 sm:p-5 bg-slate-50 min-h-screen">
|
||||||
<div className="max-w-8xl mx-auto">
|
<div className="max-w-8xl mx-auto">
|
||||||
{/* Profile Header */}
|
{/* Profile Header */}
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden mb-6">
|
<div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden mb-4 sm:mb-6">
|
||||||
<div className="p-5 flex flex-col lg:flex-row items-start gap-5">
|
<div className="p-4 sm:p-5 flex flex-col sm:flex-row items-start gap-4 sm:gap-5">
|
||||||
<div className="relative group">
|
<div className="relative group shrink-0">
|
||||||
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center shadow-lg">
|
<div className="w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center shadow-lg">
|
||||||
<span className="text-2xl font-bold text-white">{investor.name.charAt(0)}</span>
|
<span className="text-xl sm:text-2xl font-bold text-white">{investor.name.charAt(0)}</span>
|
||||||
</div>
|
</div>
|
||||||
<label className="absolute bottom-0 right-0 w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity shadow-lg border-2 border-white">
|
<label className="absolute bottom-0 right-0 w-7 h-7 sm:w-8 sm:h-8 bg-blue-600 rounded-full flex items-center justify-center cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity shadow-lg border-2 border-white">
|
||||||
<Camera className="w-4 h-4 text-white" />
|
<Camera className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-white" />
|
||||||
<input type="file" accept="image/*" className="hidden" />
|
<input type="file" accept="image/*" className="hidden" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1 w-full">
|
||||||
<div className="flex flex-wrap items-center gap-2 mb-2">
|
<div className="flex flex-wrap items-center gap-2 mb-2">
|
||||||
<h1 className="text-2xl font-bold text-slate-800">{investor.name}</h1>
|
<h1 className="text-xl sm:text-2xl font-bold text-slate-800">{investor.name}</h1>
|
||||||
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${investor.kycStatus === 'verified' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
|
<span className={`px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full text-xs font-medium ${investor.kycStatus === 'verified' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`}>
|
||||||
<ShieldCheck className="w-3 h-3 inline mr-1" /> KYC {investor.kycStatus}
|
<ShieldCheck className="w-3 h-3 inline mr-1" /> KYC {investor.kycStatus}
|
||||||
</span>
|
</span>
|
||||||
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${investor.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'}`}>
|
<span className={`px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full text-xs font-medium ${investor.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'}`}>
|
||||||
{investor.status}
|
{investor.status}
|
||||||
</span>
|
</span>
|
||||||
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${investor.riskLevel === 'low' ? 'bg-green-100 text-green-700' : investor.riskLevel === 'medium' ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-700'}`}>
|
<span className={`px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full text-xs font-medium ${investor.riskLevel === 'low' ? 'bg-green-100 text-green-700' : investor.riskLevel === 'medium' ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-700'}`}>
|
||||||
Risk: {investor.riskLevel}
|
Risk: {investor.riskLevel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-slate-500 mb-2">{investor.id} • {investor.email}</p>
|
<p className="text-xs sm:text-sm text-slate-500 mb-2">{investor.id} • {investor.email}</p>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||||
<span className="text-xs font-medium px-2 py-0.5 rounded bg-purple-50 text-purple-700">Ref: {investor.referralCode}</span>
|
<span className="text-xs font-medium px-2 py-0.5 rounded bg-purple-50 text-purple-700">Ref: {investor.referralCode}</span>
|
||||||
<span className="text-xs text-slate-400">{investor.totalReferrals} Referrals</span>
|
<span className="text-xs text-slate-400">{investor.totalReferrals} Referrals</span>
|
||||||
<span className="text-xs text-slate-400">• {investor.investments?.length || 0} Investments</span>
|
<span className="text-xs text-slate-400">• {investor.investments?.length || 0} Investments</span>
|
||||||
@@ -160,55 +162,56 @@ export default function InvestorProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-slate-100 grid grid-cols-2 md:grid-cols-6 divide-x divide-slate-100 bg-slate-50/50">
|
<div className="border-t border-slate-100 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 divide-x divide-slate-100 bg-slate-50/50">
|
||||||
<div className="p-4 text-center">
|
<div className="p-3 sm:p-4 text-center">
|
||||||
<p className="text-xs text-purple-600 font-medium">Invested</p>
|
<p className="text-xs text-purple-600 font-medium">Invested</p>
|
||||||
<p className="text-lg font-bold text-slate-800">৳{investor.totalInvested.toLocaleString()}</p>
|
<p className="text-base sm:text-lg font-bold text-slate-800">৳{investor.totalInvested.toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 text-center">
|
<div className="p-3 sm:p-4 text-center">
|
||||||
<p className="text-xs text-green-600 font-medium">Earnings</p>
|
<p className="text-xs text-green-600 font-medium">Earnings</p>
|
||||||
<p className="text-lg font-bold text-green-600">৳{investor.totalEarnings.toLocaleString()}</p>
|
<p className="text-base sm:text-lg font-bold text-green-600">৳{investor.totalEarnings.toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 text-center">
|
<div className="p-3 sm:p-4 text-center">
|
||||||
<p className="text-xs text-slate-600 font-medium">Withdrawn</p>
|
<p className="text-xs text-slate-600 font-medium">Withdrawn</p>
|
||||||
<p className="text-lg font-bold text-slate-800">৳{investor.totalWithdrawn.toLocaleString()}</p>
|
<p className="text-base sm:text-lg font-bold text-slate-800">৳{investor.totalWithdrawn.toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 text-center bg-white border-x border-slate-100">
|
<div className="p-3 sm:p-4 text-center bg-white border-x border-slate-100">
|
||||||
<p className="text-xs text-investor font-medium">Balance</p>
|
<p className="text-xs text-investor font-medium">Balance</p>
|
||||||
<p className="text-lg font-bold text-investor">৳{currentBalance.toLocaleString()}</p>
|
<p className="text-base sm:text-lg font-bold text-investor">৳{currentBalance.toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 text-center">
|
<div className="p-3 sm:p-4 text-center">
|
||||||
<p className="text-xs text-slate-600 font-medium">Bikes</p>
|
<p className="text-xs text-slate-600 font-medium">Bikes</p>
|
||||||
<p className="text-lg font-bold text-slate-800">{investor.activeBikes}</p>
|
<p className="text-base sm:text-lg font-bold text-slate-800">{investor.activeBikes}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 text-center">
|
<div className="p-3 sm:p-4 text-center">
|
||||||
<p className="text-xs text-amber-600 font-medium">Pending</p>
|
<p className="text-xs text-amber-600 font-medium">Pending</p>
|
||||||
<p className="text-lg font-bold text-amber-600">৳{investor.pendingEarnings.toLocaleString()}</p>
|
<p className="text-base sm:text-lg font-bold text-amber-600">৳{investor.pendingEarnings.toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-100 mb-6">
|
<div className="bg-white rounded-xl shadow-sm border border-slate-100 mb-4 sm:mb-6">
|
||||||
<div className="border-b border-slate-100 flex overflow-x-auto">
|
<div className="border-b border-slate-100 p-2 flex overflow-x-auto scrollbar-hide justify-between sm:justify-start sm:gap-2 gap-auto ">
|
||||||
{tabs.map(tab => {
|
{tabs.map(tab => {
|
||||||
const Icon = tab.icon;
|
const Icon = tab.icon;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
className={`px-4 py-3 text-sm font-medium whitespace-nowrap flex items-center gap-2 ${activeTab === tab.id ? 'border-b-2 border-investor text-investor' : 'text-slate-500 hover:text-slate-700'}`}
|
className={`shrink-0 flex items-center justify-center w-12 sm:w-auto px-3 sm:px-4 py-2.5 rounded-lg text-xs sm:text-sm font-medium transition-all ${activeTab === tab.id ? 'bg-investor text-white shadow-md' : 'text-slate-500 hover:bg-slate-100'}`}
|
||||||
>
|
>
|
||||||
<Icon className="w-4 h-4" /> {tab.label}
|
<Icon className="w-4 h-4 sm:mr-2" />
|
||||||
|
<span className="hidden sm:inline">{tab.label}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-5">
|
<div className="p-4 sm:p-5">
|
||||||
{/* Personal Info Tab */}
|
{/* Personal Info Tab */}
|
||||||
{activeTab === 'personal' && (
|
{activeTab === 'personal' && (
|
||||||
<div className="flex gap-4">
|
<div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
|
||||||
<div className="flex-1 space-y-4">
|
<div className="flex-1 space-y-4">
|
||||||
<SectionCard title="Personal Information" icon={User} headerBg="bg-emerald-50" headerBorder="border-emerald-100"
|
<SectionCard title="Personal Information" icon={User} headerBg="bg-emerald-50" headerBorder="border-emerald-100"
|
||||||
editKey="personal" editingSection={editingSection} setEditingSection={setEditingSection}
|
editKey="personal" editingSection={editingSection} setEditingSection={setEditingSection}
|
||||||
@@ -229,7 +232,7 @@ export default function InvestorProfilePage() {
|
|||||||
})}
|
})}
|
||||||
editForm={editForm} setEditForm={setEditForm}>
|
editForm={editForm} setEditForm={setEditForm}>
|
||||||
{editingSection === 'personal' ? (
|
{editingSection === 'personal' ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs text-slate-500 mb-1 block">Full Name</label>
|
<label className="text-xs text-slate-500 mb-1 block">Full Name</label>
|
||||||
<input type="text" value={editForm.fullName || ''} onChange={(e) => setEditForm({ ...editForm, fullName: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
<input type="text" value={editForm.fullName || ''} onChange={(e) => setEditForm({ ...editForm, fullName: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm" />
|
||||||
@@ -546,7 +549,7 @@ export default function InvestorProfilePage() {
|
|||||||
|
|
||||||
{/* Nominee Tab */}
|
{/* Nominee Tab */}
|
||||||
{activeTab === 'nominee' && (
|
{activeTab === 'nominee' && (
|
||||||
<div className="flex gap-4">
|
<div className="flex flex-col lg:flex-row gap-4 lg:gap-6">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<SectionCard title="Nominee Details" icon={Heart} headerBg="bg-orange-50" headerBorder="border-orange-100"
|
<SectionCard title="Nominee Details" icon={Heart} headerBg="bg-orange-50" headerBorder="border-orange-100"
|
||||||
editKey="nominee" editingSection={editingSection} setEditingSection={setEditingSection}
|
editKey="nominee" editingSection={editingSection} setEditingSection={setEditingSection}
|
||||||
@@ -844,7 +847,7 @@ export default function InvestorProfilePage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="w-full py-3 border-2 border-dashed border-slate-200 rounded-lg text-slate-500 hover:border-investor hover:text-investor transition-colors flex items-center justify-center gap-2">
|
<button onClick={() => setShowUploadDocModal(true)} className="w-full py-3 border-2 border-dashed border-slate-200 rounded-lg text-slate-500 hover:border-investor hover:text-investor transition-colors flex items-center justify-center gap-2">
|
||||||
<Upload className="w-4 h-4" /> Upload Document
|
<Upload className="w-4 h-4" /> Upload Document
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -854,7 +857,7 @@ export default function InvestorProfilePage() {
|
|||||||
|
|
||||||
{/* Security Tab */}
|
{/* Security Tab */}
|
||||||
{activeTab === 'security' && (
|
{activeTab === 'security' && (
|
||||||
<div className="grid lg:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SectionCard title="Change Password" icon={Lock} headerBg="bg-slate-50" headerBorder="border-slate-100">
|
<SectionCard title="Change Password" icon={Lock} headerBg="bg-slate-50" headerBorder="border-slate-100">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -1101,6 +1104,92 @@ export default function InvestorProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showUploadDocModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||||||
|
<div className="p-5 border-b border-slate-100 flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-bold text-slate-800">Upload Document</h3>
|
||||||
|
<button onClick={() => setShowUploadDocModal(false)} className="p-1.5 hover:bg-slate-100 rounded-lg">
|
||||||
|
<X className="w-5 h-5 text-slate-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">Document Type *</label>
|
||||||
|
<select
|
||||||
|
value={uploadDocForm.docType}
|
||||||
|
onChange={(e) => setUploadDocForm({ ...uploadDocForm, docType: e.target.value })}
|
||||||
|
className="w-full px-3 py-2.5 border border-slate-200 rounded-lg text-sm bg-white"
|
||||||
|
>
|
||||||
|
<option value="">Select Document Type</option>
|
||||||
|
<option value="nid">National ID (NID)</option>
|
||||||
|
<option value="passport">Passport</option>
|
||||||
|
<option value="driving_license">Driving License</option>
|
||||||
|
<option value="tax_certificate">Tax Certificate</option>
|
||||||
|
<option value="birth_certificate">Birth Certificate</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">Document Number</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={uploadDocForm.docNumber}
|
||||||
|
onChange={(e) => setUploadDocForm({ ...uploadDocForm, docNumber: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||||
|
placeholder="Enter document number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-500 mb-1 block">Upload File *</label>
|
||||||
|
<div className="border-2 border-dashed border-slate-200 rounded-lg p-6 text-center hover:border-investor transition-colors cursor-pointer"
|
||||||
|
onClick={() => document.getElementById('docFileInput')?.click()}
|
||||||
|
>
|
||||||
|
{uploadDocForm.docFile ? (
|
||||||
|
<div className="flex items-center justify-center gap-2 text-green-600">
|
||||||
|
<FileText className="w-5 h-5" />
|
||||||
|
<span className="text-sm font-medium">{uploadDocForm.docFile.name}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Upload className="w-8 h-8 mx-auto mb-2 text-slate-300" />
|
||||||
|
<p className="text-sm text-slate-500">Click to upload or drag and drop</p>
|
||||||
|
<p className="text-xs text-slate-400 mt-1">PDF, JPG, PNG (max 5MB)</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
id="docFileInput"
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.jpg,.jpeg,.png"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) setUploadDocForm({ ...uploadDocForm, docFile: file });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-5 border-t border-slate-100 flex justify-end gap-3 bg-slate-50">
|
||||||
|
<button onClick={() => setShowUploadDocModal(false)} className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm font-medium hover:bg-white">Cancel</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (!uploadDocForm.docType || !uploadDocForm.docFile) {
|
||||||
|
toast.error('Please fill all required fields');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success('Document uploaded successfully!');
|
||||||
|
setShowUploadDocModal(false);
|
||||||
|
setUploadDocForm({ docType: '', docNumber: '', docFile: null });
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 bg-investor text-white rounded-lg text-sm font-medium hover:bg-investor-dark"
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user