complete api integration

This commit is contained in:
S M Fahim Hossen 2025-10-09 15:08:05 +06:00
parent 085417ab7e
commit b7b4b4d3c5
10 changed files with 1267 additions and 437 deletions

2
.env
View file

@ -1 +1 @@
VITE_SERVER_URL=https://backend.planpostai.com/api/v2
VITE_SERVER_URL=https://backendv2.planpostai.com/api/v2

View file

@ -0,0 +1,276 @@
import React, { useState } from "react";
import { X, CreditCard, Smartphone } from "lucide-react";
import { usePaymentStore } from "@/stores/paymentStore";
import toast from "react-hot-toast";
interface PaymentMethodModalProps {
isOpen: boolean;
onClose: () => void;
}
const PaymentMethodModal: React.FC<PaymentMethodModalProps> = ({
isOpen,
onClose,
}) => {
const [paymentType, setPaymentType] = useState<"bkash" | "bank">("bkash");
const [isProcessing, setIsProcessing] = useState(false);
const { savePaymentInfo } = usePaymentStore();
// Bkash form state
const [bkashNumber, setBkashNumber] = useState("");
// Bank form state
const [bankName, setBankName] = useState("");
const [accountNumber, setAccountNumber] = useState("");
const [branch, setBranch] = useState("");
const [routing, setRouting] = useState("");
const [accountName, setAccountName] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsProcessing(true);
try {
const paymentData: Record<string, string> = {};
if (paymentType === "bkash") {
if (!bkashNumber || bkashNumber.length < 11) {
toast.error("Please enter a valid 11-digit Bkash number");
setIsProcessing(false);
return;
}
paymentData.bkash_number = bkashNumber;
} else {
if (
!bankName ||
!accountNumber ||
!branch ||
!routing ||
!accountName
) {
toast.error("Please fill in all bank details");
setIsProcessing(false);
return;
}
paymentData.bank_details = `Bank Name: ${bankName}, Account No: ${accountNumber}, Branch: ${branch}, Routing: ${routing}, Account Name: ${accountName}`;
}
const result = await savePaymentInfo(paymentData);
if (result.success) {
toast.success("Payment method added successfully!");
onClose();
// Reset form
setBkashNumber("");
setBankName("");
setAccountNumber("");
setBranch("");
setRouting("");
setAccountName("");
} else {
toast.error(result.error || "Failed to add payment method");
}
} catch {
toast.error("An error occurred. Please try again.");
} finally {
setIsProcessing(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl max-h-[90vh] overflow-y-auto max-w-md w-full">
{/* Modal Header */}
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-gray-100">
<div>
<h3 className="text-lg sm:text-xl font-bold text-gray-900">
Add Payment Method
</h3>
<p className="text-xs sm:text-sm text-gray-600">
Choose your preferred payment method
</p>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-gray-600" />
</button>
</div>
{/* Modal Body */}
<form
onSubmit={handleSubmit}
className="p-4 sm:p-6 space-y-4 sm:space-y-5"
>
{/* Payment Type Selection */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Payment Method
</label>
<div className="grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => setPaymentType("bkash")}
className={`p-4 border-2 rounded-lg transition-all ${
paymentType === "bkash"
? "border-pink-500 bg-pink-50"
: "border-gray-200 hover:border-gray-300"
}`}
>
<Smartphone
className={`w-6 h-6 mx-auto mb-2 ${
paymentType === "bkash" ? "text-pink-600" : "text-gray-400"
}`}
/>
<div className="text-sm font-semibold text-gray-900">Bkash</div>
</button>
<button
type="button"
onClick={() => setPaymentType("bank")}
className={`p-4 border-2 rounded-lg transition-all ${
paymentType === "bank"
? "border-indigo-500 bg-indigo-50"
: "border-gray-200 hover:border-gray-300"
}`}
>
<CreditCard
className={`w-6 h-6 mx-auto mb-2 ${
paymentType === "bank" ? "text-indigo-600" : "text-gray-400"
}`}
/>
<div className="text-sm font-semibold text-gray-900">Bank</div>
</button>
</div>
</div>
{/* Bkash Form */}
{paymentType === "bkash" && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Bkash Number
</label>
<input
type="tel"
value={bkashNumber}
onChange={(e) => setBkashNumber(e.target.value)}
placeholder="01XXXXXXXXX"
maxLength={11}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-pink-500 focus:border-transparent outline-none"
required
/>
<p className="text-xs text-gray-500 mt-1">
Enter your 11-digit Bkash number
</p>
</div>
)}
{/* Bank Form */}
{paymentType === "bank" && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Bank Name
</label>
<input
type="text"
value={bankName}
onChange={(e) => setBankName(e.target.value)}
placeholder="e.g., BRAC Bank"
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Account Name
</label>
<input
type="text"
value={accountName}
onChange={(e) => setAccountName(e.target.value)}
placeholder="Full name on account"
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Account Number
</label>
<input
type="text"
value={accountNumber}
onChange={(e) => setAccountNumber(e.target.value)}
placeholder="Enter account number"
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none"
required
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Branch
</label>
<input
type="text"
value={branch}
onChange={(e) => setBranch(e.target.value)}
placeholder="e.g., Gulshan"
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Routing Number
</label>
<input
type="text"
value={routing}
onChange={(e) => setRouting(e.target.value)}
placeholder="Routing #"
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none"
required
/>
</div>
</div>
</div>
)}
</form>
{/* Modal Footer */}
<div className="p-4 sm:p-6 border-t border-gray-100 flex gap-3">
<button
type="button"
onClick={onClose}
className="flex-1 px-4 py-2.5 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors font-semibold text-gray-700"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={isProcessing}
className="flex-1 px-4 py-2.5 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all font-semibold disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isProcessing ? (
<>
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Saving...
</>
) : (
"Add Payment Method"
)}
</button>
</div>
</div>
</div>
);
};
export default PaymentMethodModal;

View file

@ -10,7 +10,7 @@ const DashboardLayout: React.FC = () => {
<SidebarInset>
<div className="flex-1">
{/* Mobile Header with Sidebar Trigger */}
<header className="sticky top-0 z-10 flex h-[70px] items-center gap-2 px-2">
<header className="sticky top-0 z-10 flex h-[70px] w-[50px] items-center gap-2 px-2">
<SidebarTrigger className="hover:bg-gray-100" />
</header>

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import {
DollarSign,
TrendingUp,
@ -12,6 +12,7 @@ import {
CheckCircle,
X,
AlertCircle,
XCircle,
} from "lucide-react";
import {
XAxis,
@ -22,89 +23,104 @@ import {
Area,
AreaChart,
} from "recharts";
import { useAffiliateStore } from "@/stores/affiliateStore";
import { useWithdrawalStore } from "@/stores/withdrawStore";
import PaymentMethodModal from "@/components/PaymentModal";
import toast from "react-hot-toast";
const Earnings: React.FC = () => {
const [timeframe, setTimeframe] = useState<"7d" | "30d" | "90d" | "1y">(
"30d"
);
const [showPayoutModal, setShowPayoutModal] = useState(false);
const [payoutAmount, setPayoutAmount] = useState("890");
const [payoutAmount, setPayoutAmount] = useState("");
const [isProcessing, setIsProcessing] = useState(false);
const [showPaymentModal, setShowPaymentModal] = useState(false);
const [withdrawalNote, setWithdrawalNote] = useState("");
const earningsData = [
{ date: "Sep 1", amount: 120 },
{ date: "Sep 5", amount: 180 },
{ date: "Sep 10", amount: 240 },
{ date: "Sep 15", amount: 210 },
{ date: "Sep 20", amount: 290 },
{ date: "Sep 25", amount: 350 },
{ date: "Sep 30", amount: 450 },
];
const {
data: affiliateData,
fetchEarnings,
isLoading: earningsLoading,
} = useAffiliateStore();
const {
withdrawals,
fetchWithdrawals,
requestWithdrawal,
isLoading: withdrawalsLoading,
} = useWithdrawalStore();
const transactionData = [
{
type: "Commission",
amount: "$120",
date: "Sep 28, 2025",
status: "Completed",
ref: "REF-4521",
},
{
type: "Bonus",
amount: "$50",
date: "Sep 25, 2025",
status: "Completed",
ref: "BONUS-892",
},
{
type: "Commission",
amount: "$85",
date: "Sep 22, 2025",
status: "Completed",
ref: "REF-4489",
},
{
type: "Commission",
amount: "$95",
date: "Sep 18, 2025",
status: "Pending",
ref: "REF-4455",
},
{
type: "Commission",
amount: "$100",
date: "Sep 15, 2025",
status: "Completed",
ref: "REF-4421",
},
];
useEffect(() => {
const userStr = localStorage.getItem("aff-user");
if (userStr) {
const userData = JSON.parse(userStr);
if (userData.id) {
fetchEarnings(userData.id);
fetchWithdrawals();
}
}
}, [fetchEarnings, fetchWithdrawals]);
// Get summary data
const totalEarnings = affiliateData?.summary.total_earning || 0;
const totalWithdraw = affiliateData?.summary.total_withdraw || 0;
// Convert negative pending amount to positive
const pendingAmount = Math.abs(affiliateData?.summary.pending_amount || 0);
const availableBalance = Math.max(totalEarnings - totalWithdraw, 0);
// Calculate monthly earnings (last transaction)
const lastTransaction = affiliateData?.history[0];
const monthlyEarnings = lastTransaction?.amount || 0;
// Transform affiliate history to chart data
const earningsData =
affiliateData?.history
.slice()
.reverse()
.map((item) => ({
date: new Date(item.created_at).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
}),
amount: item.amount,
})) || [];
// Calculate stats
const totalTransactions = affiliateData?.total || 0;
const avgCommission =
totalTransactions > 0 ? totalEarnings / totalTransactions : 0;
const stats = [
{
label: "Total Earnings",
value: "$12,450",
change: "+12.5%",
trend: "up",
value: `${totalEarnings.toFixed(2)}`,
change:
totalWithdraw > 0
? `${totalWithdraw.toFixed(2)} withdrawn`
: "No withdrawals yet",
trend: "up" as const,
icon: DollarSign,
color: "from-green-500 to-emerald-600",
bgColor: "bg-green-50",
textColor: "text-green-600",
},
{
label: "This Month",
value: "$2,340",
change: "+8.2%",
trend: "up",
label: "Last Transaction",
value: `${monthlyEarnings.toFixed(2)}`,
change: lastTransaction
? new Date(lastTransaction.created_at).toLocaleDateString()
: "No transactions",
trend: "up" as const,
icon: TrendingUp,
color: "from-blue-500 to-indigo-600",
bgColor: "bg-blue-50",
textColor: "text-blue-600",
},
{
label: "Pending Payout",
value: "$450",
change: "Oct 15",
trend: "neutral",
label: "Pending Amount",
value: `${Math.max(pendingAmount, 0).toFixed(2)}`,
change: "To be processed",
trend: "neutral" as const,
icon: Clock,
color: "from-orange-500 to-amber-600",
bgColor: "bg-orange-50",
@ -112,9 +128,9 @@ const Earnings: React.FC = () => {
},
{
label: "Available Balance",
value: "$890",
change: "Ready now",
trend: "neutral",
value: `${availableBalance.toFixed(2)}`,
change: availableBalance >= 50 ? "Ready now" : `Minimum $50 required`,
trend: "neutral" as const,
icon: Wallet,
color: "from-purple-500 to-pink-600",
bgColor: "bg-purple-50",
@ -122,17 +138,94 @@ const Earnings: React.FC = () => {
},
];
const handlePayoutRequest = () => {
const handlePayoutRequest = async () => {
const amount = Number(payoutAmount);
// Validation
if (amount < 50) {
toast.error("Minimum withdrawal amount is ৳50");
return;
}
if (amount > availableBalance) {
toast.error("Insufficient balance");
return;
}
if (
!affiliateData?.account_info?.bank_details &&
!affiliateData?.account_info?.bkash_number
) {
toast.error("Please add a payment method first");
return;
}
setIsProcessing(true);
setTimeout(() => {
try {
const note =
withdrawalNote.trim() || `Withdrawal request for ৳${amount.toFixed(2)}`;
const result = await requestWithdrawal(amount, note);
if (result.success) {
toast.success("Withdrawal request submitted successfully!");
setShowPayoutModal(false);
setPayoutAmount("");
setWithdrawalNote(""); // Clear the note
// Refresh withdrawals and earnings data
fetchWithdrawals();
const userStr = localStorage.getItem("aff-user");
if (userStr) {
const userData = JSON.parse(userStr);
if (userData.id) {
fetchEarnings(userData.id);
}
}
} else {
toast.error(result.error || "Failed to submit withdrawal request");
}
} catch {
toast.error("An error occurred. Please try again.");
} finally {
setIsProcessing(false);
setShowPayoutModal(false);
alert(
"Payout request submitted successfully! You'll receive confirmation via email."
);
}, 2000);
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case "paid":
return <CheckCircle className="w-3 h-3" />;
case "cancelled":
return <XCircle className="w-3 h-3" />;
default:
return <Clock className="w-3 h-3" />;
}
};
const getStatusStyle = (status: string) => {
switch (status) {
case "paid":
return "bg-green-100 text-green-700";
case "cancelled":
return "bg-red-100 text-red-700";
default:
return "bg-yellow-100 text-yellow-700";
}
};
const isLoading = earningsLoading || withdrawalsLoading;
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Loading...</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 p-4 sm:p-6">
<div className="max-w-7xl mx-auto">
@ -152,8 +245,12 @@ const Earnings: React.FC = () => {
<span className="hidden sm:inline">Export</span>
</button>
<button
onClick={() => setShowPayoutModal(true)}
className="flex-1 sm:flex-initial px-4 sm:px-6 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all shadow-md flex items-center justify-center gap-2 text-sm sm:text-base"
onClick={() => {
setPayoutAmount(availableBalance.toFixed(2));
setShowPayoutModal(true);
}}
disabled={availableBalance < 50}
className="flex-1 sm:flex-initial px-4 sm:px-6 py-2 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all shadow-md flex items-center justify-center gap-2 text-sm sm:text-base disabled:opacity-50 disabled:cursor-not-allowed"
>
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5" />
<span className="hidden sm:inline">Request Payout</span>
@ -188,9 +285,6 @@ const Earnings: React.FC = () => {
) : (
<ArrowDownRight className="w-3 h-3 sm:w-4 sm:h-4" />
)}
<span className="text-xs sm:text-sm font-semibold">
{stat.change}
</span>
</div>
)}
</div>
@ -200,9 +294,7 @@ const Earnings: React.FC = () => {
<div className="text-xs sm:text-sm text-gray-600">
{stat.label}
</div>
{stat.trend === "neutral" && (
<div className="text-xs text-gray-500 mt-1">{stat.change}</div>
)}
<div className="text-xs text-gray-500 mt-1">{stat.change}</div>
</div>
))}
</div>
@ -212,28 +304,32 @@ const Earnings: React.FC = () => {
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex items-start gap-3 sm:gap-4">
<div className="w-12 h-12 sm:w-14 sm:h-14 bg-white bg-opacity-20 backdrop-blur-sm rounded-xl flex items-center justify-center flex-shrink-0">
<Calendar className="w-6 h-6 sm:w-7 sm:h-7 text-black" />
<Calendar className="w-6 h-6 sm:w-7 sm:h-7 text-white" />
</div>
<div>
<div className="text-indigo-100 text-xs sm:text-sm mb-1">
Next Scheduled Payout
Total Withdrawn
</div>
<div className="text-xl sm:text-3xl font-bold text-white mb-2">
October 15, 2025
{totalWithdraw.toFixed(2)}
</div>
<div className="text-indigo-100 text-xs sm:text-sm">
Your earnings will be processed automatically
{withdrawals?.total || 0} withdrawal requests
</div>
</div>
</div>
<div className="bg-white bg-opacity-20 backdrop-blur-sm rounded-xl p-4 text-center md:text-right">
<div className="text-black text-xs sm:text-sm mb-1">
Estimated Amount
Available Balance
</div>
<div className="text-3xl sm:text-4xl font-bold text-black">
$450
{availableBalance.toFixed(2)}
</div>
<div className="text-black text-xs mt-1">
{availableBalance >= 50
? "Ready to withdraw"
: "Minimum ৳50 required"}
</div>
<div className="text-black text-xs mt-1">+ pending approvals</div>
</div>
</div>
</div>
@ -266,65 +362,131 @@ const Earnings: React.FC = () => {
))}
</div>
</div>
<ResponsiveContainer width="100%" height={250}>
<AreaChart data={earningsData}>
<defs>
<linearGradient id="colorAmount" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="date"
stroke="#9ca3af"
style={{ fontSize: "10px" }}
/>
<YAxis stroke="#9ca3af" style={{ fontSize: "10px" }} />
<Tooltip
contentStyle={{
backgroundColor: "#fff",
border: "1px solid #e5e7eb",
borderRadius: "8px",
boxShadow: "0 4px 6px rgba(0,0,0,0.1)",
}}
/>
<Area
type="monotone"
dataKey="amount"
stroke="#6366f1"
strokeWidth={2}
fill="url(#colorAmount)"
/>
</AreaChart>
</ResponsiveContainer>
{earningsData.length > 0 ? (
<ResponsiveContainer width="100%" height={250}>
<AreaChart data={earningsData}>
<defs>
<linearGradient
id="colorAmount"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="date"
stroke="#9ca3af"
style={{ fontSize: "10px" }}
/>
<YAxis stroke="#9ca3af" style={{ fontSize: "10px" }} />
<Tooltip
contentStyle={{
backgroundColor: "#fff",
border: "1px solid #e5e7eb",
borderRadius: "8px",
boxShadow: "0 4px 6px rgba(0,0,0,0.1)",
}}
/>
<Area
type="monotone"
dataKey="amount"
stroke="#6366f1"
strokeWidth={2}
fill="url(#colorAmount)"
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="h-64 flex items-center justify-center text-gray-500">
No earnings data available
</div>
)}
</div>
{/* Payment Methods */}
{/* Payment Methods */}
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 sm:p-6">
<h2 className="text-lg sm:text-xl font-semibold text-gray-900 mb-4">
Payment Method
Payment Methods
</h2>
<div className="space-y-3">
<div className="p-3 sm:p-4 border-2 border-indigo-600 rounded-lg bg-indigo-50">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-indigo-600 rounded-lg flex items-center justify-center flex-shrink-0">
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 text-sm sm:text-base">
PayPal
{/* Bank Details */}
{affiliateData?.account_info?.bank_details && (
<div className="p-3 sm:p-4 border-2 border-indigo-600 rounded-lg bg-indigo-50">
<div className="flex items-start gap-3">
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-indigo-600 rounded-lg flex items-center justify-center flex-shrink-0">
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
</div>
<div className="text-xs sm:text-sm text-gray-600 truncate">
john@example.com
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 text-sm sm:text-base mb-1">
Bank Account
</div>
<div className="text-xs sm:text-sm text-gray-600">
{affiliateData.account_info.bank_details}
</div>
</div>
<CheckCircle className="w-4 h-4 sm:w-5 sm:h-5 text-indigo-600 flex-shrink-0" />
</div>
<CheckCircle className="w-4 h-4 sm:w-5 sm:h-5 text-indigo-600 flex-shrink-0" />
</div>
</div>
<button className="w-full p-3 sm:p-4 border-2 border-dashed border-gray-300 rounded-lg text-gray-600 hover:border-indigo-600 hover:text-indigo-600 transition-colors font-medium text-sm sm:text-base">
+ Add Payment Method
</button>
)}
{/* Bkash Number */}
{affiliateData?.account_info?.bkash_number && (
<div className="p-3 sm:p-4 border-2 border-pink-600 rounded-lg bg-pink-50">
<div className="flex items-center gap-3">
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-pink-600 rounded-lg flex items-center justify-center flex-shrink-0">
<svg
className="w-5 h-5 text-white"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 text-sm sm:text-base">
Bkash
</div>
<div className="text-xs sm:text-sm text-gray-600 truncate">
{affiliateData.account_info.bkash_number}
</div>
</div>
<CheckCircle className="w-4 h-4 sm:w-5 sm:h-5 text-pink-600 flex-shrink-0" />
</div>
</div>
)}
{/* Add Payment Method Button - Only show if no payment methods exist */}
{!affiliateData?.account_info?.bank_details &&
!affiliateData?.account_info?.bkash_number && (
<div className="p-4 border-2 border-dashed border-gray-300 rounded-lg text-center">
<p className="text-gray-500 text-sm mb-3">
No payment method added yet
</p>
<button
onClick={() => setShowPaymentModal(true)}
className="w-full p-3 sm:p-4 border-2 border-dashed border-gray-300 rounded-lg text-gray-600 hover:border-indigo-600 hover:text-indigo-600 transition-colors font-medium text-sm sm:text-base"
>
+ Add Payment Method
</button>
</div>
)}
{/* Add Another Payment Method - Show if at least one exists */}
{(affiliateData?.account_info?.bank_details ||
affiliateData?.account_info?.bkash_number) && (
<button
onClick={() => setShowPaymentModal(true)}
className="w-full p-3 sm:p-4 border-2 border-dashed border-gray-300 rounded-lg text-gray-600 hover:border-indigo-600 hover:text-indigo-600 transition-colors font-medium text-sm sm:text-base"
>
+ Add Another Payment Method
</button>
)}
</div>
<div className="mt-4 sm:mt-6 pt-4 sm:pt-6 border-t border-gray-100">
@ -337,7 +499,7 @@ const Earnings: React.FC = () => {
Total Transactions
</span>
<span className="font-semibold text-gray-900 text-sm sm:text-base">
142
{totalTransactions}
</span>
</div>
<div className="flex justify-between">
@ -345,15 +507,15 @@ const Earnings: React.FC = () => {
Avg. Commission
</span>
<span className="font-semibold text-gray-900 text-sm sm:text-base">
$87.68
{avgCommission.toFixed(2)}
</span>
</div>
<div className="flex justify-between">
<span className="text-xs sm:text-sm text-gray-600">
Success Rate
Withdrawals
</span>
<span className="font-semibold text-green-600 text-sm sm:text-base">
98.2%
<span className="font-semibold text-gray-900 text-sm sm:text-base">
{withdrawals?.total || 0}
</span>
</div>
</div>
@ -365,80 +527,86 @@ const Earnings: React.FC = () => {
<div className="bg-white rounded-xl shadow-sm border border-gray-100">
<div className="p-4 sm:p-6 border-b border-gray-100">
<h2 className="text-lg sm:text-xl font-semibold text-gray-900">
Recent Transactions
Withdrawal History
</h2>
<p className="text-xs sm:text-sm text-gray-600 mt-1">
Your latest earnings and payouts
Your withdrawal requests and status
</p>
</div>
<div className="divide-y divide-gray-100">
{transactionData.map((transaction, idx) => (
<div
key={idx}
className="p-4 sm:p-6 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3 sm:gap-4 min-w-0">
<div
className={`w-10 h-10 sm:w-12 sm:h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
transaction.type === "Bonus"
? "bg-purple-100"
: "bg-green-100"
}`}
>
<DollarSign
className={`w-5 h-5 sm:w-6 sm:h-6 ${
transaction.type === "Bonus"
? "text-purple-600"
: "text-green-600"
{withdrawals?.data && withdrawals.data.length > 0 ? (
withdrawals.data.map((transaction, idx) => (
<div
key={idx}
className="p-4 sm:p-6 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3 sm:gap-4 min-w-0">
<div
className={`w-10 h-10 sm:w-12 sm:h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${
transaction.status === "paid"
? "bg-green-100"
: transaction.status === "cancelled"
? "bg-red-100"
: "bg-yellow-100"
}`}
/>
</div>
<div className="min-w-0">
<div className="font-semibold text-gray-900 text-sm sm:text-base">
{transaction.type}
>
<DollarSign
className={`w-5 h-5 sm:w-6 sm:h-6 ${
transaction.status === "paid"
? "text-green-600"
: transaction.status === "cancelled"
? "text-red-600"
: "text-yellow-600"
}`}
/>
</div>
<div className="text-xs sm:text-sm text-gray-600">
{transaction.date}
</div>
<div className="text-xs text-gray-500 mt-1">
{transaction.ref}
<div className="min-w-0">
<div className="font-semibold text-gray-900 text-sm sm:text-base">
Withdrawal Request
</div>
<div className="text-xs sm:text-sm text-gray-600">
{new Date(
transaction.created_at
).toLocaleDateString()}
</div>
<div className="text-xs text-gray-500 mt-1">
{transaction.note}
</div>
</div>
</div>
</div>
<div className="text-right flex-shrink-0">
<div className="text-lg sm:text-2xl font-bold text-gray-900">
{transaction.amount}
</div>
<span
className={`inline-flex items-center gap-1 px-2 py-0.5 sm:py-1 rounded-full text-xs font-semibold mt-1 ${
transaction.status === "Completed"
? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{transaction.status === "Completed" ? (
<CheckCircle className="w-3 h-3" />
) : (
<Clock className="w-3 h-3" />
)}
<span className="hidden sm:inline">
{transaction.status}
<div className="text-right flex-shrink-0">
<div className="text-lg sm:text-2xl font-bold text-gray-900">
{transaction.amount.toFixed(2)}
</div>
<span
className={`inline-flex items-center gap-1 px-2 py-0.5 sm:py-1 rounded-full text-xs font-semibold mt-1 ${getStatusStyle(
transaction.status
)}`}
>
{getStatusIcon(transaction.status)}
<span className="hidden sm:inline capitalize">
{transaction.status}
</span>
</span>
</span>
</div>
</div>
</div>
))
) : (
<div className="p-8 text-center text-gray-500">
No withdrawal history yet
</div>
))}
</div>
<div className="p-4 sm:p-6 border-t border-gray-100 text-center">
<button className="text-indigo-600 hover:text-indigo-700 font-semibold text-xs sm:text-sm">
View All Transactions
</button>
)}
</div>
</div>
</div>
<PaymentMethodModal
isOpen={showPaymentModal}
onClose={() => setShowPaymentModal(false)}
/>
{/* Payout Request Modal */}
{showPayoutModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
@ -466,6 +634,7 @@ const Earnings: React.FC = () => {
</button>
</div>
{/* Modal Body */}
{/* Modal Body */}
<div className="p-4 sm:p-6 space-y-4 sm:space-y-5">
{/* Available Balance */}
@ -474,7 +643,7 @@ const Earnings: React.FC = () => {
Available Balance
</div>
<div className="text-2xl sm:text-3xl font-bold text-green-900">
$890.00
{availableBalance.toFixed(2)}
</div>
<div className="text-xs text-green-600 mt-1">
Ready for withdrawal
@ -493,18 +662,36 @@ const Earnings: React.FC = () => {
value={payoutAmount}
onChange={(e) => setPayoutAmount(e.target.value)}
placeholder="0.00"
max="890"
max={availableBalance}
className="w-full pl-10 pr-16 sm:pr-20 py-2.5 sm:py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none text-base sm:text-lg font-semibold"
/>
<button
onClick={() => setPayoutAmount("890")}
onClick={() => setPayoutAmount(availableBalance.toFixed(2))}
className="absolute right-2 top-1/2 transform -translate-y-1/2 px-2.5 sm:px-3 py-1 bg-indigo-600 text-white text-xs sm:text-sm rounded-md hover:bg-indigo-700 transition-colors"
>
Max
</button>
</div>
<p className="text-xs text-gray-500 mt-1">
Minimum withdrawal: $50.00
Minimum withdrawal: 50.00
</p>
</div>
{/* Note Input */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Note (Optional)
</label>
<textarea
value={withdrawalNote}
onChange={(e) => setWithdrawalNote(e.target.value)}
placeholder="Add a note for this withdrawal..."
rows={3}
maxLength={200}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none text-sm resize-none"
/>
<p className="text-xs text-gray-500 mt-1">
{withdrawalNote.length}/200 characters
</p>
</div>
@ -513,22 +700,55 @@ const Earnings: React.FC = () => {
<label className="block text-sm font-medium text-gray-700 mb-2">
Payment Method
</label>
<div className="p-3 sm:p-4 border-2 border-indigo-600 rounded-lg bg-indigo-50">
<div className="flex items-center gap-3">
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-indigo-600 rounded-lg flex items-center justify-center">
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 text-sm sm:text-base">
PayPal
{affiliateData?.account_info?.bank_details && (
<div className="p-3 sm:p-4 border-2 border-indigo-600 rounded-lg bg-indigo-50 mb-3">
<div className="flex items-start gap-3">
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-indigo-600 rounded-lg flex items-center justify-center">
<CreditCard className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
</div>
<div className="text-xs sm:text-sm text-gray-600 truncate">
john@example.com
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 text-sm sm:text-base">
Bank Account
</div>
<div className="text-xs sm:text-sm text-gray-600">
{affiliateData.account_info.bank_details}
</div>
</div>
<CheckCircle className="w-5 h-5 text-indigo-600" />
</div>
<CheckCircle className="w-5 h-5 text-indigo-600" />
</div>
</div>
)}
{affiliateData?.account_info?.bkash_number && (
<div className="p-3 sm:p-4 border-2 border-pink-600 rounded-lg bg-pink-50">
<div className="flex items-center gap-3">
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-pink-600 rounded-lg flex items-center justify-center">
<svg
className="w-5 h-5 text-white"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z" />
</svg>
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-gray-900 text-sm sm:text-base">
Bkash
</div>
<div className="text-xs sm:text-sm text-gray-600 truncate">
{affiliateData.account_info.bkash_number}
</div>
</div>
<CheckCircle className="w-5 h-5 text-pink-600" />
</div>
</div>
)}
{!affiliateData?.account_info?.bank_details &&
!affiliateData?.account_info?.bkash_number && (
<div className="p-4 border-2 border-dashed border-gray-300 rounded-lg text-center text-gray-500 text-sm">
No payment method added. Please add a payment method
first.
</div>
)}
</div>
{/* Processing Info */}
@ -548,19 +768,19 @@ const Earnings: React.FC = () => {
<div className="flex justify-between text-xs sm:text-sm">
<span className="text-gray-600">Withdrawal Amount</span>
<span className="font-semibold text-gray-900">
${payoutAmount}
{Number(payoutAmount).toFixed(2)}
</span>
</div>
<div className="flex justify-between text-xs sm:text-sm">
<span className="text-gray-600">Processing Fee</span>
<span className="font-semibold text-gray-900">$0.00</span>
<span className="font-semibold text-gray-900">0.00</span>
</div>
<div className="border-t border-gray-200 pt-2 flex justify-between">
<span className="font-semibold text-gray-900 text-sm sm:text-base">
You'll Receive
</span>
<span className="text-lg sm:text-xl font-bold text-indigo-600">
${payoutAmount}
{Number(payoutAmount).toFixed(2)}
</span>
</div>
</div>
@ -579,7 +799,10 @@ const Earnings: React.FC = () => {
disabled={
isProcessing ||
Number(payoutAmount) < 50 ||
Number(payoutAmount) > 890
Number(Number(payoutAmount).toFixed(2)) >
Number(availableBalance.toFixed(2)) ||
(!affiliateData?.account_info?.bank_details &&
!affiliateData?.account_info?.bkash_number)
}
className="flex-1 px-3 sm:px-4 py-2.5 sm:py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white rounded-lg hover:from-indigo-700 hover:to-purple-700 transition-all font-semibold disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 text-sm sm:text-base"
>
@ -589,7 +812,7 @@ const Earnings: React.FC = () => {
<span className="hidden sm:inline">Processing...</span>
</>
) : (
<>Confirm</>
<>Confirm Withdrawal</>
)}
</button>
</div>

View file

@ -82,12 +82,23 @@ const Overview = () => {
// Calculate metrics from affiliate data
const totalEarnings = affiliateData?.summary.total_earning || 0;
const totalWithdraw = affiliateData?.summary.total_withdraw || 0;
const pendingAmount = affiliateData?.summary.pending_amount || 0;
const totalReferrals = affiliateData?.total || 0;
// Convert negative pending amount to positive
const pendingAmount = Math.abs(affiliateData?.summary.pending_amount || 0);
// Calculate average commission
// Get unique referred users count
const uniqueReferredUsers = affiliateData?.history
? new Set(affiliateData.history.map((h) => h.referred_user.id)).size
: 0;
// Total transactions count
const totalTransactions = affiliateData?.total || 0;
// Calculate average commission per transaction
const averageCommission =
totalReferrals > 0 ? totalEarnings / totalReferrals : 0;
totalTransactions > 0 ? totalEarnings / totalTransactions : 0;
// Calculate available balance (can't be negative)
const availableBalance = Math.max(totalEarnings - totalWithdraw, 0);
const isLoading = userLoading || earningsLoading;
@ -115,26 +126,26 @@ const Overview = () => {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 mb-6 sm:mb-8">
<MetricsCard
title="Total Earnings"
value={`$${totalEarnings.toFixed(2)}`}
value={`${totalEarnings.toFixed(2)}`}
icon={DollarSign}
bgColor="bg-gradient-to-br from-blue-500 to-blue-600"
/>
<MetricsCard
title="Total Referrals"
value={totalReferrals}
subtitle={`${affiliateData?.history.length || 0} transactions`}
value={uniqueReferredUsers}
subtitle={`${totalTransactions} transactions`}
icon={Users}
bgColor="bg-gradient-to-br from-purple-500 to-purple-600"
/>
<MetricsCard
title="Total Withdrawn"
value={`$${totalWithdraw.toFixed(2)}`}
value={`${totalWithdraw.toFixed(2)}`}
icon={TrendingUp}
bgColor="bg-gradient-to-br from-green-500 to-green-600"
/>
<MetricsCard
title="Pending Commission"
value={`$${Math.max(pendingAmount, 0).toFixed(2)}`}
value={`${Math.max(pendingAmount, 0).toFixed(2)}`}
icon={Clock}
bgColor="bg-gradient-to-br from-orange-500 to-orange-600"
/>
@ -192,9 +203,11 @@ const Overview = () => {
Average Commission
</h3>
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
${averageCommission.toFixed(2)}
{averageCommission.toFixed(2)}
</p>
<p className="text-xs sm:text-sm text-gray-500 mt-2">
Per transaction
</p>
<p className="text-xs sm:text-sm text-gray-500 mt-2">Per referral</p>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 sm:p-6">
@ -202,10 +215,12 @@ const Overview = () => {
Available Balance
</h3>
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
${(totalEarnings - totalWithdraw).toFixed(2)}
{availableBalance.toFixed(2)}
</p>
<p className="text-xs sm:text-sm text-green-600 mt-2">
Ready to withdraw
{availableBalance >= 50
? "Ready to withdraw"
: "Minimum ৳50 required"}
</p>
</div>
@ -214,7 +229,7 @@ const Overview = () => {
Latest Transaction
</h3>
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
${affiliateData?.history[0]?.amount.toFixed(2) || "0.00"}
{affiliateData?.history[0]?.amount.toFixed(2) || "0.00"}
</p>
<p className="text-xs sm:text-sm text-gray-500 mt-2">
{affiliateData?.history[0]

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import {
Users,
DollarSign,
@ -13,75 +13,30 @@ import {
Clock,
MoreVertical,
} from "lucide-react";
interface Referral {
name: string;
email: string;
date: string;
earnings: string;
status: "Active" | "Pending";
signupDate: string;
totalCommission: string;
country: string;
}
const referrals: Referral[] = [
{
name: "John Doe",
email: "john@example.com",
date: "2025-09-20",
earnings: "$50",
status: "Active",
signupDate: "2025-08-15",
totalCommission: "$450",
country: "United States",
},
{
name: "Jane Smith",
email: "jane@example.com",
date: "2025-09-18",
earnings: "$20",
status: "Pending",
signupDate: "2025-09-10",
totalCommission: "$20",
country: "Canada",
},
{
name: "Michael Chen",
email: "michael@example.com",
date: "2025-09-15",
earnings: "$120",
status: "Active",
signupDate: "2025-07-22",
totalCommission: "$890",
country: "Singapore",
},
{
name: "Sarah Johnson",
email: "sarah@example.com",
date: "2025-09-12",
earnings: "$85",
status: "Active",
signupDate: "2025-06-30",
totalCommission: "$1,240",
country: "United Kingdom",
},
{
name: "David Williams",
email: "david@example.com",
date: "2025-09-25",
earnings: "$15",
status: "Pending",
signupDate: "2025-09-20",
totalCommission: "$15",
country: "Australia",
},
];
import { useUserStore } from "@/stores/userStore";
import { useAffiliateStore } from "@/stores/affiliateStore";
import { useReferredUsersStore } from "@/stores/referredUserStore";
const Referrals: React.FC = () => {
const [searchTerm, setSearchTerm] = useState("");
const [copied, setCopied] = useState(false);
const referralLink = "https://affiliatepro.com/ref/ABC123";
const { user } = useUserStore();
const { data: affiliateData, fetchEarnings } = useAffiliateStore();
const { referredUsers, fetchReferredUsers, isLoading } =
useReferredUsersStore();
useEffect(() => {
const userStr = localStorage.getItem("aff-user");
if (userStr) {
const userData = JSON.parse(userStr);
if (userData.id) {
fetchEarnings(userData.id);
fetchReferredUsers();
}
}
}, [fetchEarnings, fetchReferredUsers]);
const referralLink = `https://dashboard.planpostai.com/sign-up?ref=${user?.referral_code}`;
const handleCopy = () => {
navigator.clipboard.writeText(referralLink);
@ -89,42 +44,65 @@ const Referrals: React.FC = () => {
setTimeout(() => setCopied(false), 2000);
};
// Calculate stats
const totalReferrals = referredUsers?.total || 0;
const activeReferrals =
referredUsers?.data.filter((u) => u.has_used_referral_discount).length || 0;
const totalEarnings = affiliateData?.summary.total_earning || 0;
const avgPerReferral =
totalReferrals > 0 ? totalEarnings / totalReferrals : 0;
const stats = [
{
label: "Total Referrals",
value: "5",
value: totalReferrals.toString(),
icon: Users,
color: "bg-blue-500",
change: "+12% from last month",
change: `${referredUsers?.data.length || 0} users`,
},
{
label: "Active Referrals",
value: "3",
value: activeReferrals.toString(),
icon: CheckCircle,
color: "bg-green-500",
change: "60% conversion rate",
change:
totalReferrals > 0
? `${Math.round(
(activeReferrals / totalReferrals) * 100
)}% conversion rate`
: "0% conversion rate",
},
{
label: "Total Earnings",
value: "$2,615",
value: `${totalEarnings.toFixed(2)}`,
icon: DollarSign,
color: "bg-purple-500",
change: "+$450 this month",
change: `From referrals`,
},
{
label: "Avg. Per Referral",
value: "$523",
value: `${avgPerReferral.toFixed(2)}`,
icon: TrendingUp,
color: "bg-orange-500",
change: "+18% growth",
change: "Average commission",
},
];
const filteredReferrals = referrals.filter(
(r) =>
r.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
r.email.toLowerCase().includes(searchTerm.toLowerCase())
);
const filteredReferrals =
referredUsers?.data.filter(
(r) =>
r.first_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
r.last_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
r.email.toLowerCase().includes(searchTerm.toLowerCase())
) || [];
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-lg">Loading...</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 p-4">
@ -178,7 +156,7 @@ const Referrals: React.FC = () => {
Share this link to earn commissions
</p>
<div className="flex flex-col sm:flex-row gap-3">
<div className="flex-1 bg-white bg-opacity-20 backdrop-blur-sm rounded-lg px-3 sm:px-4 py-2.5 sm:py-3 text-white font-mono text-xs sm:text-sm break-all">
<div className="flex-1 bg-white bg-opacity-20 backdrop-blur-sm rounded-lg px-3 sm:px-4 py-2.5 sm:py-3 text-black font-mono text-xs sm:text-sm break-all">
{referralLink}
</div>
<button
@ -235,68 +213,69 @@ const Referrals: React.FC = () => {
{/* Mobile Card View */}
<div className="block lg:hidden">
{filteredReferrals.map((r, idx) => (
<div
key={idx}
className="p-4 border-b border-gray-100 hover:bg-gray-50 transition-colors"
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center text-white font-semibold text-sm">
{r.name.charAt(0)}
{filteredReferrals.length === 0 ? (
<div className="p-8 text-center text-gray-500">
No referrals found
</div>
) : (
filteredReferrals.map((r, idx) => (
<div
key={idx}
className="p-4 border-b border-gray-100 hover:bg-gray-50 transition-colors"
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center text-white font-semibold text-sm">
{r.first_name.charAt(0)}
</div>
<div>
<div className="font-semibold text-gray-900 text-sm">
{r.first_name} {r.last_name}
</div>
<div className="text-xs text-gray-500">{r.email}</div>
</div>
</div>
<span
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-semibold ${
r.has_used_referral_discount
? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{r.has_used_referral_discount ? (
<>
<CheckCircle className="w-3 h-3" />
Active
</>
) : (
<>
<Clock className="w-3 h-3" />
Pending
</>
)}
</span>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<div className="text-xs text-gray-500 mb-1">
Signup Date
</div>
<div className="text-gray-700">
{new Date(r.created_at).toLocaleDateString()}
</div>
</div>
<div>
<div className="font-semibold text-gray-900 text-sm">
{r.name}
<div className="text-xs text-gray-500 mb-1">Status</div>
<div className="text-gray-700">
{r.has_used_referral_discount
? "Used Discount"
: "Not Used"}
</div>
<div className="text-xs text-gray-500">{r.email}</div>
</div>
</div>
<span
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-semibold ${
r.status === "Active"
? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{r.status === "Active" ? (
<CheckCircle className="w-3 h-3" />
) : (
<Clock className="w-3 h-3" />
)}
{r.status}
</span>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div>
<div className="text-xs text-gray-500 mb-1">
Signup Date
</div>
<div className="text-gray-700">{r.signupDate}</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">Country</div>
<div className="text-gray-700">{r.country}</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">
Last Earning
</div>
<div className="font-semibold text-gray-900">
{r.earnings}
</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">
Total Commission
</div>
<div className="font-semibold text-gray-900">
{r.totalCommission}
</div>
</div>
</div>
</div>
))}
))
)}
</div>
{/* Desktop Table View */}
@ -311,13 +290,7 @@ const Referrals: React.FC = () => {
Signup Date
</th>
<th className="text-left p-4 text-sm font-semibold text-gray-700">
Country
</th>
<th className="text-left p-4 text-sm font-semibold text-gray-700">
Last Earnings
</th>
<th className="text-left p-4 text-sm font-semibold text-gray-700">
Total Commission
Email
</th>
<th className="text-left p-4 text-sm font-semibold text-gray-700">
Status
@ -326,70 +299,70 @@ const Referrals: React.FC = () => {
</tr>
</thead>
<tbody>
{filteredReferrals.map((r, idx) => (
<tr
key={idx}
className="border-b border-gray-100 hover:bg-gray-50 transition-colors"
>
<td className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center text-white font-semibold">
{r.name.charAt(0)}
</div>
<div>
<div className="font-semibold text-gray-900">
{r.name}
</div>
<div className="text-sm text-gray-500">{r.email}</div>
</div>
</div>
</td>
<td className="p-4">
<div className="flex items-center gap-2 text-gray-700">
<Calendar className="w-4 h-4 text-gray-400" />
<span className="text-sm">{r.signupDate}</span>
</div>
</td>
<td className="p-4">
<span className="text-sm text-gray-700">{r.country}</span>
</td>
<td className="p-4">
<div className="flex items-center gap-1">
<DollarSign className="w-4 h-4 text-green-600" />
<span className="font-semibold text-gray-900">
{r.earnings}
</span>
</div>
<div className="text-xs text-gray-500">{r.date}</div>
</td>
<td className="p-4">
<span className="font-semibold text-gray-900">
{r.totalCommission}
</span>
</td>
<td className="p-4">
<span
className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-semibold ${
r.status === "Active"
? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{r.status === "Active" ? (
<CheckCircle className="w-3 h-3" />
) : (
<Clock className="w-3 h-3" />
)}
{r.status}
</span>
</td>
<td className="p-4">
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
<MoreVertical className="w-5 h-5 text-gray-400" />
</button>
{filteredReferrals.length === 0 ? (
<tr>
<td colSpan={5} className="p-8 text-center text-gray-500">
No referrals found
</td>
</tr>
))}
) : (
filteredReferrals.map((r, idx) => (
<tr
key={idx}
className="border-b border-gray-100 hover:bg-gray-50 transition-colors"
>
<td className="p-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center text-white font-semibold">
{r.first_name.charAt(0)}
</div>
<div>
<div className="font-semibold text-gray-900">
{r.first_name} {r.last_name}
</div>
</div>
</div>
</td>
<td className="p-4">
<div className="flex items-center gap-2 text-gray-700">
<Calendar className="w-4 h-4 text-gray-400" />
<span className="text-sm">
{new Date(r.created_at).toLocaleDateString()}
</span>
</div>
</td>
<td className="p-4">
<span className="text-sm text-gray-700">{r.email}</span>
</td>
<td className="p-4">
<span
className={`inline-flex items-center gap-1 px-3 py-1 rounded-full text-xs font-semibold ${
r.has_used_referral_discount
? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700"
}`}
>
{r.has_used_referral_discount ? (
<>
<CheckCircle className="w-3 h-3" />
Active
</>
) : (
<>
<Clock className="w-3 h-3" />
Pending
</>
)}
</span>
</td>
<td className="p-4">
<button className="p-2 hover:bg-gray-100 rounded-lg transition-colors">
<MoreVertical className="w-5 h-5 text-gray-400" />
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
@ -399,7 +372,7 @@ const Referrals: React.FC = () => {
<div className="text-xs sm:text-sm text-gray-600">
Showing{" "}
<span className="font-semibold">{filteredReferrals.length}</span>{" "}
of <span className="font-semibold">{referrals.length}</span>{" "}
of <span className="font-semibold">{totalReferrals}</span>{" "}
referrals
</div>
<div className="flex gap-2">

View file

@ -31,6 +31,10 @@ interface AffiliateData {
success: boolean;
affiliate: Affiliate;
summary: AffiliateSummary;
account_info?: {
bank_details?: string;
bkash_number?: string;
};
history: EarningHistory[];
total: number;
page: number;

View file

@ -0,0 +1,85 @@
// store/paymentStore.ts
import { create } from "zustand";
interface PaymentInfo {
bkash_number?: string;
bank_details?: string;
}
interface PaymentState {
paymentInfo: PaymentInfo | null;
isLoading: boolean;
error: string | null;
// Actions
savePaymentInfo: (
data: PaymentInfo
) => Promise<{ success: boolean; error?: string }>;
clearError: () => void;
}
const API_BASE_URL = import.meta.env.VITE_SERVER_URL;
export const usePaymentStore = create<PaymentState>((set) => ({
paymentInfo: null,
isLoading: false,
error: null,
savePaymentInfo: async (data: PaymentInfo) => {
set({ isLoading: true, error: null });
try {
const token = localStorage.getItem("aff-token");
if (!token) {
throw new Error("No authentication token found");
}
const response = await fetch(`${API_BASE_URL}/affiliate/payment-info`, {
method: "POST",
headers: {
accept: "*/*",
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Failed to save payment info");
}
await response.json();
set({
paymentInfo: data,
isLoading: false,
error: null,
});
return { success: true };
} catch (error) {
console.error("Save payment info error:", error);
set({
isLoading: false,
error:
error instanceof Error
? error.message
: "Failed to save payment info",
});
return {
success: false,
error:
error instanceof Error
? error.message
: "Failed to save payment info",
};
}
},
clearError: () => {
set({ error: null });
},
}));

View file

@ -0,0 +1,96 @@
// store/referredUsersStore.ts
import { create } from "zustand";
interface ReferredUser {
id: string;
first_name: string;
last_name: string;
email: string;
created_at: string;
has_used_referral_discount: boolean;
}
interface ReferredUsersData {
success: boolean;
message: string;
data: ReferredUser[];
page: number;
limit: number;
total: number;
}
interface ReferredUsersState {
referredUsers: ReferredUsersData | null;
isLoading: boolean;
error: string | null;
// Actions
fetchReferredUsers: (page?: number, limit?: number) => Promise<void>;
clearData: () => void;
clearError: () => void;
}
const API_BASE_URL = import.meta.env.VITE_SERVER_URL;
export const useReferredUsersStore = create<ReferredUsersState>((set) => ({
referredUsers: null,
isLoading: false,
error: null,
fetchReferredUsers: async (page = 1, limit = 10) => {
set({ isLoading: true, error: null });
try {
// Get token from localStorage
const token = localStorage.getItem("aff-token");
if (!token) {
throw new Error("No authentication token found");
}
const response = await fetch(
`${API_BASE_URL}/affiliate/referred-users?page=${page}&limit=${limit}`,
{
method: "GET",
headers: {
accept: "*/*",
Authorization: `Bearer ${token}`,
},
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Failed to fetch referred users");
}
const data = await response.json();
set({
referredUsers: data,
isLoading: false,
error: null,
});
} catch (error) {
console.error("Fetch referred users error:", error);
set({
isLoading: false,
error:
error instanceof Error
? error.message
: "Failed to fetch referred users",
});
}
},
clearData: () => {
set({
referredUsers: null,
error: null,
});
},
clearError: () => {
set({ error: null });
},
}));

158
src/stores/withdrawStore.ts Normal file
View file

@ -0,0 +1,158 @@
// store/withdrawalStore.ts
import { create } from "zustand";
interface Withdrawal {
id: number;
user: string;
created_at: string;
amount: number;
status: "pending" | "paid" | "cancelled";
note: string;
}
interface WithdrawalData {
success: boolean;
page: number;
limit: number;
total: number;
data: Withdrawal[];
}
interface WithdrawalState {
withdrawals: WithdrawalData | null;
isLoading: boolean;
error: string | null;
// Actions
fetchWithdrawals: (page?: number, limit?: number) => Promise<void>;
requestWithdrawal: (
amount: number,
note: string
) => Promise<{ success: boolean; error?: string }>;
clearData: () => void;
clearError: () => void;
}
const API_BASE_URL = import.meta.env.VITE_SERVER_URL;
export const useWithdrawalStore = create<WithdrawalState>((set) => ({
withdrawals: null,
isLoading: false,
error: null,
fetchWithdrawals: async (page = 1, limit = 10) => {
set({ isLoading: true, error: null });
try {
// Get token from localStorage
const token = localStorage.getItem("aff-token");
if (!token) {
throw new Error("No authentication token found");
}
const response = await fetch(
`${API_BASE_URL}/affiliate/withdraw/my-withdraws?page=${page}&limit=${limit}`,
{
method: "GET",
headers: {
accept: "*/*",
Authorization: `Bearer ${token}`,
},
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Failed to fetch withdrawals");
}
const data = await response.json();
set({
withdrawals: data,
isLoading: false,
error: null,
});
} catch (error) {
console.error("Fetch withdrawals error:", error);
set({
isLoading: false,
error:
error instanceof Error
? error.message
: "Failed to fetch withdrawals",
});
}
},
requestWithdrawal: async (amount: number, note: string) => {
set({ isLoading: true, error: null });
try {
const token = localStorage.getItem("aff-token");
if (!token) {
throw new Error("No authentication token found");
}
const response = await fetch(
`${API_BASE_URL}/affiliate/withdraw/request`,
{
method: "POST",
headers: {
accept: "*/*",
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount,
note,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Failed to request withdrawal");
}
await response.json();
set({
isLoading: false,
error: null,
});
return { success: true };
} catch (error) {
console.error("Request withdrawal error:", error);
set({
isLoading: false,
error:
error instanceof Error
? error.message
: "Failed to request withdrawal",
});
return {
success: false,
error:
error instanceof Error
? error.message
: "Failed to request withdrawal",
};
}
},
clearData: () => {
set({
withdrawals: null,
error: null,
});
},
clearError: () => {
set({ error: null });
},
}));