232 lines
7.7 KiB
TypeScript
232 lines
7.7 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import {
|
|
Copy,
|
|
TrendingUp,
|
|
Users,
|
|
DollarSign,
|
|
Clock,
|
|
CheckCircle2,
|
|
} from "lucide-react";
|
|
import { useUserStore } from "@/stores/userStore";
|
|
import { useAffiliateStore } from "@/stores/affiliateStore";
|
|
|
|
type MetricsCardProps = {
|
|
title: string;
|
|
value: string | number;
|
|
subtitle?: string;
|
|
icon: React.ElementType;
|
|
trend?: string;
|
|
bgColor: string;
|
|
};
|
|
|
|
const MetricsCard = ({
|
|
title,
|
|
value,
|
|
subtitle,
|
|
icon: Icon,
|
|
trend,
|
|
bgColor,
|
|
}: MetricsCardProps) => (
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 sm:p-6 hover:shadow-md transition-shadow">
|
|
<div className="flex items-start justify-between mb-3 sm:mb-4">
|
|
<div className={`${bgColor} p-2 sm:p-3 rounded-lg`}>
|
|
<Icon className="w-5 h-5 sm:w-6 sm:h-6 text-white" />
|
|
</div>
|
|
{trend && (
|
|
<span className="flex items-center text-xs sm:text-sm font-medium text-green-600 bg-green-50 px-2 py-1 rounded-full">
|
|
<TrendingUp className="w-3 h-3 mr-1" />
|
|
{trend}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<h3 className="text-gray-600 text-xs sm:text-sm font-medium mb-1">
|
|
{title}
|
|
</h3>
|
|
<p className="text-xl sm:text-2xl font-bold text-gray-900">{value}</p>
|
|
{subtitle && (
|
|
<p className="text-xs sm:text-sm text-gray-500 mt-1">{subtitle}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const Overview = () => {
|
|
const [copied, setCopied] = useState(false);
|
|
const { user, fetchUser, isLoading: userLoading } = useUserStore();
|
|
const {
|
|
data: affiliateData,
|
|
fetchEarnings,
|
|
isLoading: earningsLoading,
|
|
} = useAffiliateStore();
|
|
|
|
useEffect(() => {
|
|
// Get user ID from localStorage
|
|
const userStr = localStorage.getItem("aff-user");
|
|
|
|
if (userStr) {
|
|
const userData = JSON.parse(userStr);
|
|
if (userData.id) {
|
|
fetchUser(userData.id);
|
|
fetchEarnings(userData.id);
|
|
}
|
|
}
|
|
}, [fetchUser, fetchEarnings]);
|
|
|
|
const referralLink = `https://dashboard.planpostai.com/sign-up?ref=${user?.referral_code}`;
|
|
|
|
const handleCopy = () => {
|
|
navigator.clipboard.writeText(referralLink);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
// 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;
|
|
|
|
// Calculate average commission
|
|
const averageCommission =
|
|
totalReferrals > 0 ? totalEarnings / totalReferrals : 0;
|
|
|
|
const isLoading = userLoading || earningsLoading;
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-lg">Loading...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-0 ml-3">
|
|
{/* Header */}
|
|
<div className="mb-6 sm:mb-8">
|
|
<h3 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">
|
|
Dashboard Overview
|
|
</h3>
|
|
<p className="text-sm sm:text-base text-gray-600">
|
|
Track your affiliate performance and earnings
|
|
</p>
|
|
</div>
|
|
|
|
{/* Metrics Grid */}
|
|
<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)}`}
|
|
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`}
|
|
icon={Users}
|
|
bgColor="bg-gradient-to-br from-purple-500 to-purple-600"
|
|
/>
|
|
<MetricsCard
|
|
title="Total Withdrawn"
|
|
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)}`}
|
|
icon={Clock}
|
|
bgColor="bg-gradient-to-br from-orange-500 to-orange-600"
|
|
/>
|
|
</div>
|
|
|
|
{/* Referral Link Section */}
|
|
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl p-4 sm:p-6 shadow-sm border border-blue-100">
|
|
<div className="flex items-center gap-2 mb-3 sm:mb-4">
|
|
<div className="bg-blue-500 p-2 rounded-lg">
|
|
<Users className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
|
</div>
|
|
<h2 className="text-lg sm:text-xl font-semibold text-gray-900">
|
|
Your Referral Link
|
|
</h2>
|
|
</div>
|
|
<p className="text-gray-600 text-xs sm:text-sm mb-3 sm:mb-4">
|
|
Share this link with your network to start earning commissions
|
|
</p>
|
|
<div className="flex flex-col sm:flex-row gap-3">
|
|
<div className="flex-1 relative">
|
|
<input
|
|
type="text"
|
|
value={referralLink}
|
|
readOnly
|
|
className="w-full border-2 border-gray-200 rounded-lg px-3 sm:px-4 py-2.5 sm:py-3 bg-white focus:outline-none focus:border-blue-500 transition-colors font-mono text-xs sm:text-sm"
|
|
/>
|
|
</div>
|
|
<button
|
|
onClick={handleCopy}
|
|
className={`px-4 sm:px-6 py-2.5 sm:py-3 rounded-lg font-medium transition-all flex items-center justify-center gap-2 min-w-[100px] sm:min-w-[120px] text-sm sm:text-base ${
|
|
copied
|
|
? "bg-green-500 text-white"
|
|
: "bg-blue-600 text-white hover:bg-blue-700 active:scale-95"
|
|
}`}
|
|
>
|
|
{copied ? (
|
|
<>
|
|
<CheckCircle2 className="w-4 h-4" />
|
|
Copied!
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy className="w-4 h-4" />
|
|
Copy
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Stats */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 mt-6 sm:mt-8">
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 sm:p-6">
|
|
<h3 className="text-xs sm:text-sm font-medium text-gray-600 mb-2 sm:mb-3">
|
|
Average Commission
|
|
</h3>
|
|
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
|
|
${averageCommission.toFixed(2)}
|
|
</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">
|
|
<h3 className="text-xs sm:text-sm font-medium text-gray-600 mb-2 sm:mb-3">
|
|
Available Balance
|
|
</h3>
|
|
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
|
|
${(totalEarnings - totalWithdraw).toFixed(2)}
|
|
</p>
|
|
<p className="text-xs sm:text-sm text-green-600 mt-2">
|
|
Ready to withdraw
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 sm:p-6 sm:col-span-2 lg:col-span-1">
|
|
<h3 className="text-xs sm:text-sm font-medium text-gray-600 mb-2 sm:mb-3">
|
|
Latest Transaction
|
|
</h3>
|
|
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
|
|
${affiliateData?.history[0]?.amount.toFixed(2) || "0.00"}
|
|
</p>
|
|
<p className="text-xs sm:text-sm text-gray-500 mt-2">
|
|
{affiliateData?.history[0]
|
|
? new Date(
|
|
affiliateData.history[0].created_at
|
|
).toLocaleDateString()
|
|
: "No transactions yet"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Overview;
|