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==",
|
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.3",
|
"@babel/generator": "^7.28.3",
|
||||||
|
|
@ -2308,6 +2309,7 @@
|
||||||
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
|
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.12.0"
|
"undici-types": "~7.12.0"
|
||||||
}
|
}
|
||||||
|
|
@ -2318,6 +2320,7 @@
|
||||||
"integrity": "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==",
|
"integrity": "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
|
|
@ -2328,6 +2331,7 @@
|
||||||
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
|
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -2384,6 +2388,7 @@
|
||||||
"integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==",
|
"integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.44.1",
|
"@typescript-eslint/scope-manager": "8.44.1",
|
||||||
"@typescript-eslint/types": "8.44.1",
|
"@typescript-eslint/types": "8.44.1",
|
||||||
|
|
@ -2636,6 +2641,7 @@
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -2766,6 +2772,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.3",
|
"baseline-browser-mapping": "^2.8.3",
|
||||||
"caniuse-lite": "^1.0.30001741",
|
"caniuse-lite": "^1.0.30001741",
|
||||||
|
|
@ -2920,7 +2927,8 @@
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/d3-array": {
|
"node_modules/d3-array": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
|
|
@ -3189,6 +3197,7 @@
|
||||||
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
|
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
|
|
@ -4373,6 +4382,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
|
@ -4382,6 +4392,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
|
|
@ -4418,6 +4429,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/use-sync-external-store": "^0.0.6",
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
"use-sync-external-store": "^1.4.0"
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
|
@ -4584,7 +4596,8 @@
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/redux-thunk": {
|
"node_modules/redux-thunk": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
|
|
@ -4865,6 +4878,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -4933,6 +4947,7 @@
|
||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -5092,6 +5107,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz",
|
||||||
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
|
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
@ -5183,6 +5199,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
20
src/App.tsx
20
src/App.tsx
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
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 Login from "./pages/Login";
|
||||||
import Signup from "./pages/Signup";
|
import Signup from "./pages/Signup";
|
||||||
import DashboardLayout from "./components/layouts/DashboardLayouts";
|
import DashboardLayout from "./components/layouts/DashboardLayouts";
|
||||||
|
|
@ -8,21 +8,27 @@ import Referrals from "./pages/Referrals";
|
||||||
import Earnings from "./pages/Earnings";
|
import Earnings from "./pages/Earnings";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import Landing from "./pages/Landing";
|
import Landing from "./pages/Landing";
|
||||||
|
import ProtectedRoute from "./components/ProtectedRoute";
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="/" element={<Landing />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/signup" element={<Signup />} />
|
<Route path="/signup" element={<Signup />} />
|
||||||
<Route path="/" element={<Landing/>} />
|
|
||||||
|
|
||||||
<Route element={<DashboardLayout />}>
|
{/* Protected Dashboard Routes - Only for affiliates */}
|
||||||
<Route path="/dashboard/overview" element={<Overview />} />
|
<Route element={<ProtectedRoute />}>
|
||||||
<Route path="/dashboard/referrals" element={<Referrals />} />
|
<Route element={<DashboardLayout />}>
|
||||||
<Route path="/dashboard/earnings" element={<Earnings />} />
|
<Route path="/dashboard/overview" element={<Overview />} />
|
||||||
|
<Route path="/dashboard/referrals" element={<Referrals />} />
|
||||||
|
<Route path="/dashboard/earnings" element={<Earnings />} />
|
||||||
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
{/* Catch all - redirect to landing */}
|
||||||
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<Toaster position="top-right" reverseOrder={false} />
|
<Toaster position="top-right" reverseOrder={false} />
|
||||||
</BrowserRouter>
|
</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 {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
|
|
@ -12,10 +12,27 @@ import {
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { LayoutDashboard, Users, DollarSign, LogOut } from "lucide-react";
|
import { LayoutDashboard, Users, DollarSign, LogOut } from "lucide-react";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import { useUserStore } from "@/stores/userStore";
|
||||||
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
|
|
||||||
const AppSidebar: React.FC = () => {
|
const AppSidebar: React.FC = () => {
|
||||||
const { pathname } = useLocation();
|
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 = [
|
const links = [
|
||||||
{ name: "Overview", path: "/dashboard/overview", icon: LayoutDashboard },
|
{ name: "Overview", path: "/dashboard/overview", icon: LayoutDashboard },
|
||||||
{ name: "Referrals", path: "/dashboard/referrals", icon: Users },
|
{ name: "Referrals", path: "/dashboard/referrals", icon: Users },
|
||||||
|
|
@ -89,7 +106,10 @@ const AppSidebar: React.FC = () => {
|
||||||
asChild
|
asChild
|
||||||
className="hover:bg-red-50 text-red-600 transition-colors bg-white"
|
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} />
|
<LogOut size={20} />
|
||||||
<span className="font-medium">Logout</span>
|
<span className="font-medium">Logout</span>
|
||||||
</button>
|
</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="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">
|
<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>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-semibold text-gray-900 truncate">
|
<p className="text-sm font-semibold text-gray-900 truncate">
|
||||||
John Doe
|
{user?.first_name || "John Doe"} {user?.last_name || ""}
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from "react";
|
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 { useNavigate } from "react-router";
|
||||||
import { useAuthStore } from "@/stores/authStore";
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
|
|
||||||
|
|
@ -19,9 +19,12 @@ const Login: React.FC = () => {
|
||||||
setError("");
|
setError("");
|
||||||
const result = await login(form.email, form.password);
|
const result = await login(form.email, form.password);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success && result?.user?.role === "affiliate") {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
navigate("/dashboard/overview");
|
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">
|
<div className="w-full max-w-md">
|
||||||
{/* Mobile Logo */}
|
{/* Mobile Logo */}
|
||||||
<div className="lg:hidden flex items-center gap-3 mb-8 justify-center">
|
<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">
|
<div className="w-12 h-12 flex items-center justify-center">
|
||||||
<TrendingUp className="w-6 h-6 text-white" />
|
<img src="/planpost.png" alt="Logo" className="w-12 h-12" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-2xl font-bold text-gray-900">
|
<span className="text-2xl font-bold text-gray-900">
|
||||||
AffiliatePro
|
Planpost Affiliate
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Copy,
|
Copy,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
|
|
@ -7,6 +7,8 @@ import {
|
||||||
Clock,
|
Clock,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useUserStore } from "@/stores/userStore";
|
||||||
|
import { useAffiliateStore } from "@/stores/affiliateStore";
|
||||||
|
|
||||||
type MetricsCardProps = {
|
type MetricsCardProps = {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -49,7 +51,27 @@ const MetricsCard = ({
|
||||||
|
|
||||||
const Overview = () => {
|
const Overview = () => {
|
||||||
const [copied, setCopied] = useState(false);
|
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 = () => {
|
const handleCopy = () => {
|
||||||
navigator.clipboard.writeText(referralLink);
|
navigator.clipboard.writeText(referralLink);
|
||||||
|
|
@ -57,8 +79,28 @@ const Overview = () => {
|
||||||
setTimeout(() => setCopied(false), 2000);
|
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 (
|
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 */}
|
{/* Header */}
|
||||||
<div className="mb-6 sm:mb-8">
|
<div className="mb-6 sm:mb-8">
|
||||||
<h3 className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">
|
<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">
|
<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
|
<MetricsCard
|
||||||
title="Total Earnings"
|
title="Total Earnings"
|
||||||
value="$3,200"
|
value={`$${totalEarnings.toFixed(2)}`}
|
||||||
icon={DollarSign}
|
icon={DollarSign}
|
||||||
bgColor="bg-gradient-to-br from-blue-500 to-blue-600"
|
bgColor="bg-gradient-to-br from-blue-500 to-blue-600"
|
||||||
/>
|
/>
|
||||||
<MetricsCard
|
<MetricsCard
|
||||||
title="Total Referrals"
|
title="Total Referrals"
|
||||||
value="85"
|
value={totalReferrals}
|
||||||
subtitle="65 Active"
|
subtitle={`${affiliateData?.history.length || 0} transactions`}
|
||||||
icon={Users}
|
icon={Users}
|
||||||
bgColor="bg-gradient-to-br from-purple-500 to-purple-600"
|
bgColor="bg-gradient-to-br from-purple-500 to-purple-600"
|
||||||
/>
|
/>
|
||||||
<MetricsCard
|
<MetricsCard
|
||||||
title="This Month"
|
title="Total Withdrawn"
|
||||||
value="$820"
|
value={`$${totalWithdraw.toFixed(2)}`}
|
||||||
trend="+15%"
|
|
||||||
icon={TrendingUp}
|
icon={TrendingUp}
|
||||||
bgColor="bg-gradient-to-br from-green-500 to-green-600"
|
bgColor="bg-gradient-to-br from-green-500 to-green-600"
|
||||||
/>
|
/>
|
||||||
<MetricsCard
|
<MetricsCard
|
||||||
title="Pending Commission"
|
title="Pending Commission"
|
||||||
value="$120"
|
value={`$${Math.max(pendingAmount, 0).toFixed(2)}`}
|
||||||
icon={Clock}
|
icon={Clock}
|
||||||
bgColor="bg-gradient-to-br from-orange-500 to-orange-600"
|
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="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">
|
<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">
|
<h3 className="text-xs sm:text-sm font-medium text-gray-600 mb-2 sm:mb-3">
|
||||||
Conversion Rate
|
Average Commission
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-2xl sm:text-3xl font-bold text-gray-900 mb-2">
|
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
|
||||||
12.5%
|
${averageCommission.toFixed(2)}
|
||||||
</p>
|
</p>
|
||||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
<p className="text-xs sm:text-sm text-gray-500 mt-2">Per referral</p>
|
||||||
<div
|
|
||||||
className="bg-blue-500 h-2 rounded-full transition-all duration-500"
|
|
||||||
style={{ width: "12.5%" }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 sm:p-6">
|
<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">
|
<h3 className="text-xs sm:text-sm font-medium text-gray-600 mb-2 sm:mb-3">
|
||||||
Average Commission
|
Available Balance
|
||||||
</h3>
|
</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">
|
<p className="text-xs sm:text-sm text-green-600 mt-2">
|
||||||
↑ $2.15 from last month
|
Ready to withdraw
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<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">
|
<h3 className="text-xs sm:text-sm font-medium text-gray-600 mb-2 sm:mb-3">
|
||||||
Next Payout
|
Latest Transaction
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-2xl sm:text-3xl font-bold text-gray-900">$945</p>
|
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
|
||||||
<p className="text-xs sm:text-sm text-gray-500 mt-2">In 12 days</p>
|
${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>
|
</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: (
|
login: (
|
||||||
email: string,
|
email: string,
|
||||||
password: string
|
password: string
|
||||||
) => Promise<{ success: boolean; error?: string }>;
|
) => Promise<{ success: boolean; error?: string; user: User | null }>;
|
||||||
register: (
|
register: (
|
||||||
data: RegisterData
|
data: RegisterData
|
||||||
) => Promise<{ success: boolean; error?: string }>;
|
) => Promise<{ success: boolean; error?: string }>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
|
initializeAuth: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RegisterData {
|
interface RegisterData {
|
||||||
|
|
@ -50,6 +51,28 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
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) => {
|
login: async (email: string, password: string) => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
|
|
||||||
|
|
@ -65,6 +88,7 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
console.log("Login failed:", errorData.message);
|
||||||
throw new Error(errorData.message || "Login failed");
|
throw new Error(errorData.message || "Login failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +109,7 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true, user };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Login error:", error);
|
console.error("Login error:", error);
|
||||||
|
|
||||||
|
|
@ -93,9 +117,16 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||||
localStorage.removeItem("aff-token");
|
localStorage.removeItem("aff-token");
|
||||||
localStorage.removeItem("aff-user");
|
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 });
|
set({ isLoading: true, error: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
// Prepare JSON payload
|
||||||
|
const payload = {
|
||||||
// Required fields
|
email: userData.email,
|
||||||
formData.append("email", userData.email);
|
first_name: userData.first_name,
|
||||||
formData.append("first_name", userData.first_name);
|
last_name: userData.last_name || "",
|
||||||
formData.append("password", userData.password);
|
password: userData.password,
|
||||||
formData.append("phone", userData.phone || "");
|
phone: userData.phone || "",
|
||||||
formData.append("role", userData.role || "affiliate");
|
role: userData.role || "affiliate",
|
||||||
|
};
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}/auth/register`, {
|
const response = await fetch(`${API_BASE_URL}/auth/register`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
accept: "application/json",
|
accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: formData,
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -132,17 +165,30 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Registration error:", 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: () => {
|
logout: () => {
|
||||||
|
// Clear localStorage
|
||||||
|
localStorage.removeItem("aff-token");
|
||||||
|
localStorage.removeItem("aff-user");
|
||||||
|
|
||||||
set({
|
set({
|
||||||
token: null,
|
token: null,
|
||||||
user: null,
|
user: null,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
window.location.href = "/login";
|
||||||
},
|
},
|
||||||
|
|
||||||
clearError: () => {
|
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