update
This commit is contained in:
parent
36b121a21e
commit
085417ab7e
9 changed files with 446 additions and 58 deletions
21
package-lock.json
generated
21
package-lock.json
generated
|
|
@ -74,6 +74,7 @@
|
|||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
|
|
@ -2308,6 +2309,7 @@
|
|||
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.12.0"
|
||||
}
|
||||
|
|
@ -2318,6 +2320,7 @@
|
|||
"integrity": "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
|
|
@ -2328,6 +2331,7 @@
|
|||
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
|
|
@ -2384,6 +2388,7 @@
|
|||
"integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.44.1",
|
||||
"@typescript-eslint/types": "8.44.1",
|
||||
|
|
@ -2636,6 +2641,7 @@
|
|||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -2766,6 +2772,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.3",
|
||||
"caniuse-lite": "^1.0.30001741",
|
||||
|
|
@ -2920,7 +2927,8 @@
|
|||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
|
|
@ -3189,6 +3197,7 @@
|
|||
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -4373,6 +4382,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -4382,6 +4392,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
|
|
@ -4418,6 +4429,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
|
|
@ -4584,7 +4596,8 @@
|
|||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
|
|
@ -4865,6 +4878,7 @@
|
|||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -4933,6 +4947,7 @@
|
|||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -5092,6 +5107,7 @@
|
|||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
|
||||
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -5183,6 +5199,7 @@
|
|||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
|
|||
20
src/App.tsx
20
src/App.tsx
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { BrowserRouter, Routes, Route} from "react-router-dom";
|
||||
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
||||
import Login from "./pages/Login";
|
||||
import Signup from "./pages/Signup";
|
||||
import DashboardLayout from "./components/layouts/DashboardLayouts";
|
||||
|
|
@ -8,21 +8,27 @@ import Referrals from "./pages/Referrals";
|
|||
import Earnings from "./pages/Earnings";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import Landing from "./pages/Landing";
|
||||
import ProtectedRoute from "./components/ProtectedRoute";
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
|
||||
<Route path="/" element={<Landing />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/signup" element={<Signup />} />
|
||||
<Route path="/" element={<Landing/>} />
|
||||
|
||||
<Route element={<DashboardLayout />}>
|
||||
<Route path="/dashboard/overview" element={<Overview />} />
|
||||
<Route path="/dashboard/referrals" element={<Referrals />} />
|
||||
<Route path="/dashboard/earnings" element={<Earnings />} />
|
||||
{/* Protected Dashboard Routes - Only for affiliates */}
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route element={<DashboardLayout />}>
|
||||
<Route path="/dashboard/overview" element={<Overview />} />
|
||||
<Route path="/dashboard/referrals" element={<Referrals />} />
|
||||
<Route path="/dashboard/earnings" element={<Earnings />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* Catch all - redirect to landing */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
<Toaster position="top-right" reverseOrder={false} />
|
||||
</BrowserRouter>
|
||||
|
|
|
|||
44
src/components/ProtectedRoute.tsx
Normal file
44
src/components/ProtectedRoute.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { Navigate, Outlet } from "react-router-dom";
|
||||
|
||||
const ProtectedRoute = () => {
|
||||
const token = localStorage.getItem("aff-token");
|
||||
const userStr = localStorage.getItem("aff-user");
|
||||
|
||||
if (!token) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
|
||||
if (user.role !== "affiliate") {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<h2 className="text-2xl font-bold text-red-600 mb-2">
|
||||
Access Denied
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
This dashboard is only accessible to affiliates.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing user data:", error);
|
||||
|
||||
localStorage.removeItem("aff-token");
|
||||
localStorage.removeItem("aff-user");
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
} else {
|
||||
// No user data, redirect to login
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
|
|
@ -12,10 +12,27 @@ import {
|
|||
} from "@/components/ui/sidebar";
|
||||
import { LayoutDashboard, Users, DollarSign, LogOut } from "lucide-react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
const AppSidebar: React.FC = () => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const { user, fetchUser } = useUserStore();
|
||||
const { logout } = useAuthStore();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}, [fetchUser]);
|
||||
|
||||
const links = [
|
||||
{ name: "Overview", path: "/dashboard/overview", icon: LayoutDashboard },
|
||||
{ name: "Referrals", path: "/dashboard/referrals", icon: Users },
|
||||
|
|
@ -89,7 +106,10 @@ const AppSidebar: React.FC = () => {
|
|||
asChild
|
||||
className="hover:bg-red-50 text-red-600 transition-colors bg-white"
|
||||
>
|
||||
<button className="flex items-center gap-3 px-3 py-2.5 rounded-lg w-full">
|
||||
<button
|
||||
className="flex items-center gap-3 px-3 py-2.5 rounded-lg w-full"
|
||||
onClick={() => logout()}
|
||||
>
|
||||
<LogOut size={20} />
|
||||
<span className="font-medium">Logout</span>
|
||||
</button>
|
||||
|
|
@ -99,13 +119,13 @@ const AppSidebar: React.FC = () => {
|
|||
|
||||
<div className="mt-3 mx-2 flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-semibold">
|
||||
JD
|
||||
{user?.first_name ? user.first_name.charAt(0) : "U"}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold text-gray-900 truncate">
|
||||
John Doe
|
||||
{user?.first_name || "John Doe"} {user?.last_name || ""}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 truncate">john@example.com</p>
|
||||
<p className="text-xs text-gray-500 truncate">{user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import { TrendingUp, Mail, Lock, ArrowRight } from "lucide-react";
|
||||
import { Mail, Lock, ArrowRight } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
|
|
@ -19,9 +19,12 @@ const Login: React.FC = () => {
|
|||
setError("");
|
||||
const result = await login(form.email, form.password);
|
||||
|
||||
if (result.success) {
|
||||
if (result.success && result?.user?.role === "affiliate") {
|
||||
setIsLoading(false);
|
||||
navigate("/dashboard/overview");
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
setError(result.error || "Login failed. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -77,11 +80,11 @@ const Login: React.FC = () => {
|
|||
<div className="w-full max-w-md">
|
||||
{/* Mobile Logo */}
|
||||
<div className="lg:hidden flex items-center gap-3 mb-8 justify-center">
|
||||
<div className="w-10 h-10 bg-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<TrendingUp className="w-6 h-6 text-white" />
|
||||
<div className="w-12 h-12 flex items-center justify-center">
|
||||
<img src="/planpost.png" alt="Logo" className="w-12 h-12" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-gray-900">
|
||||
AffiliatePro
|
||||
Planpost Affiliate
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Copy,
|
||||
TrendingUp,
|
||||
|
|
@ -7,6 +7,8 @@ import {
|
|||
Clock,
|
||||
CheckCircle2,
|
||||
} from "lucide-react";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import { useAffiliateStore } from "@/stores/affiliateStore";
|
||||
|
||||
type MetricsCardProps = {
|
||||
title: string;
|
||||
|
|
@ -49,7 +51,27 @@ const MetricsCard = ({
|
|||
|
||||
const Overview = () => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const referralLink = "https://yoursite.com/ref/ABC123";
|
||||
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);
|
||||
|
|
@ -57,8 +79,28 @@ const Overview = () => {
|
|||
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">
|
||||
<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">
|
||||
|
|
@ -73,27 +115,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="$3,200"
|
||||
value={`$${totalEarnings.toFixed(2)}`}
|
||||
icon={DollarSign}
|
||||
bgColor="bg-gradient-to-br from-blue-500 to-blue-600"
|
||||
/>
|
||||
<MetricsCard
|
||||
title="Total Referrals"
|
||||
value="85"
|
||||
subtitle="65 Active"
|
||||
value={totalReferrals}
|
||||
subtitle={`${affiliateData?.history.length || 0} transactions`}
|
||||
icon={Users}
|
||||
bgColor="bg-gradient-to-br from-purple-500 to-purple-600"
|
||||
/>
|
||||
<MetricsCard
|
||||
title="This Month"
|
||||
value="$820"
|
||||
trend="+15%"
|
||||
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="$120"
|
||||
value={`$${Math.max(pendingAmount, 0).toFixed(2)}`}
|
||||
icon={Clock}
|
||||
bgColor="bg-gradient-to-br from-orange-500 to-orange-600"
|
||||
/>
|
||||
|
|
@ -148,35 +189,40 @@ const Overview = () => {
|
|||
<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">
|
||||
Conversion Rate
|
||||
Average Commission
|
||||
</h3>
|
||||
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">
|
||||
12.5%
|
||||
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
|
||||
${averageCommission.toFixed(2)}
|
||||
</p>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full transition-all duration-500"
|
||||
style={{ width: "12.5%" }}
|
||||
></div>
|
||||
</div>
|
||||
<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">
|
||||
Average Commission
|
||||
Available Balance
|
||||
</h3>
|
||||
<p className="text-2xl sm:text-3xl font-bold text-gray-900">$37.65</p>
|
||||
<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">
|
||||
↑ $2.15 from last month
|
||||
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">
|
||||
Next Payout
|
||||
Latest Transaction
|
||||
</h3>
|
||||
<p className="text-2xl sm:text-3xl font-bold text-gray-900">$945</p>
|
||||
<p className="text-xs sm:text-sm text-gray-500 mt-2">In 12 days</p>
|
||||
<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>
|
||||
|
|
|
|||
116
src/stores/affiliateStore.ts
Normal file
116
src/stores/affiliateStore.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// store/affiliateStore.ts
|
||||
import { create } from "zustand";
|
||||
|
||||
interface ReferredUser {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface EarningHistory {
|
||||
id: number;
|
||||
usage_type: string;
|
||||
amount: number;
|
||||
created_at: string;
|
||||
referred_user: ReferredUser;
|
||||
}
|
||||
|
||||
interface AffiliateSummary {
|
||||
total_earning: number;
|
||||
total_withdraw: number;
|
||||
pending_amount: number;
|
||||
}
|
||||
|
||||
interface Affiliate {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface AffiliateData {
|
||||
success: boolean;
|
||||
affiliate: Affiliate;
|
||||
summary: AffiliateSummary;
|
||||
history: EarningHistory[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
interface AffiliateState {
|
||||
data: AffiliateData | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
fetchEarnings: (
|
||||
userId: string,
|
||||
page?: number,
|
||||
limit?: number
|
||||
) => Promise<void>;
|
||||
clearData: () => void;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_SERVER_URL;
|
||||
|
||||
export const useAffiliateStore = create<AffiliateState>((set) => ({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
fetchEarnings: async (userId: string, 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/earning/${userId}?page=${page}&limit=${limit}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || "Failed to fetch earnings data");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
set({
|
||||
data,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Fetch earnings error:", error);
|
||||
set({
|
||||
isLoading: false,
|
||||
error:
|
||||
error instanceof Error ? error.message : "Failed to fetch earnings",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
clearData: () => {
|
||||
set({
|
||||
data: null,
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
|
||||
clearError: () => {
|
||||
set({ error: null });
|
||||
},
|
||||
}));
|
||||
|
|
@ -20,12 +20,13 @@ interface AuthState {
|
|||
login: (
|
||||
email: string,
|
||||
password: string
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
) => Promise<{ success: boolean; error?: string; user: User | null }>;
|
||||
register: (
|
||||
data: RegisterData
|
||||
) => Promise<{ success: boolean; error?: string }>;
|
||||
logout: () => void;
|
||||
clearError: () => void;
|
||||
initializeAuth: () => void;
|
||||
}
|
||||
|
||||
interface RegisterData {
|
||||
|
|
@ -50,6 +51,28 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
// Initialize auth state from localStorage
|
||||
initializeAuth: () => {
|
||||
try {
|
||||
const token = localStorage.getItem("aff-token");
|
||||
const userStr = localStorage.getItem("aff-user");
|
||||
|
||||
if (token && userStr) {
|
||||
const user = JSON.parse(userStr);
|
||||
set({
|
||||
token,
|
||||
user,
|
||||
isAuthenticated: true,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize auth:", error);
|
||||
// Clear invalid data
|
||||
localStorage.removeItem("aff-token");
|
||||
localStorage.removeItem("aff-user");
|
||||
}
|
||||
},
|
||||
|
||||
login: async (email: string, password: string) => {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
|
|
@ -65,6 +88,7 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
console.log("Login failed:", errorData.message);
|
||||
throw new Error(errorData.message || "Login failed");
|
||||
}
|
||||
|
||||
|
|
@ -85,7 +109,7 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||
error: null,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
return { success: true, user };
|
||||
} catch (error) {
|
||||
console.error("Login error:", error);
|
||||
|
||||
|
|
@ -93,9 +117,16 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||
localStorage.removeItem("aff-token");
|
||||
localStorage.removeItem("aff-user");
|
||||
|
||||
set({ isLoading: false, error: "Login failed" });
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
return { success: false, error: "Login failed" };
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
user: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -103,21 +134,23 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
|
||||
// Required fields
|
||||
formData.append("email", userData.email);
|
||||
formData.append("first_name", userData.first_name);
|
||||
formData.append("password", userData.password);
|
||||
formData.append("phone", userData.phone || "");
|
||||
formData.append("role", userData.role || "affiliate");
|
||||
// Prepare JSON payload
|
||||
const payload = {
|
||||
email: userData.email,
|
||||
first_name: userData.first_name,
|
||||
last_name: userData.last_name || "",
|
||||
password: userData.password,
|
||||
phone: userData.phone || "",
|
||||
role: userData.role || "affiliate",
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/auth/register`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: formData,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -132,17 +165,30 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Registration error:", error);
|
||||
return { success: false, error: "Registration error" };
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : "Registration error",
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Registration error",
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
logout: () => {
|
||||
// Clear localStorage
|
||||
localStorage.removeItem("aff-token");
|
||||
localStorage.removeItem("aff-user");
|
||||
|
||||
set({
|
||||
token: null,
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
error: null,
|
||||
});
|
||||
window.location.href = "/login";
|
||||
},
|
||||
|
||||
clearError: () => {
|
||||
|
|
|
|||
90
src/stores/userStore.ts
Normal file
90
src/stores/userStore.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { create } from "zustand";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name?: string;
|
||||
role: string;
|
||||
phone?: string;
|
||||
city?: string;
|
||||
country?: string;
|
||||
address?: string;
|
||||
profile_photo?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
referral_code?: string;
|
||||
}
|
||||
|
||||
interface UserState {
|
||||
user: User | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
fetchUser: (userId: string) => Promise<void>;
|
||||
clearUser: () => void;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_SERVER_URL;
|
||||
|
||||
export const useUserStore = create<UserState>((set) => ({
|
||||
user: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
fetchUser: async (userId: string) => {
|
||||
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}/users/${userId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || "Failed to fetch user data");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Handle different response structures
|
||||
const userData = data.data || data.user || data;
|
||||
|
||||
set({
|
||||
user: userData,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Fetch user error:", error);
|
||||
set({
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : "Failed to fetch user",
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
clearUser: () => {
|
||||
set({
|
||||
user: null,
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
|
||||
clearError: () => {
|
||||
set({ error: null });
|
||||
},
|
||||
}));
|
||||
Loading…
Add table
Reference in a new issue