This commit is contained in:
S M Fahim Hossen 2025-10-08 16:04:20 +06:00
parent 36b121a21e
commit 085417ab7e
9 changed files with 446 additions and 58 deletions

21
package-lock.json generated
View file

@ -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"
},

View file

@ -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>

View 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;

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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 });
},
}));

View file

@ -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
View 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 });
},
}));