diff --git a/package-lock.json b/package-lock.json index 904f900..826e02b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, diff --git a/src/App.tsx b/src/App.tsx index 7bec0b6..a512f4c 100644 --- a/src/App.tsx +++ b/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 ( - + } /> } /> } /> - } /> - }> - } /> - } /> - } /> + {/* Protected Dashboard Routes - Only for affiliates */} + }> + }> + } /> + } /> + } /> + + + {/* Catch all - redirect to landing */} + } /> diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..9f89876 --- /dev/null +++ b/src/components/ProtectedRoute.tsx @@ -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 ; + } + + if (userStr) { + try { + const user = JSON.parse(userStr); + + if (user.role !== "affiliate") { + return ( +
+
+

+ Access Denied +

+

+ This dashboard is only accessible to affiliates. +

+
+
+ ); + } + } catch (error) { + console.error("Error parsing user data:", error); + + localStorage.removeItem("aff-token"); + localStorage.removeItem("aff-user"); + return ; + } + } else { + // No user data, redirect to login + return ; + } + + return ; +}; + +export default ProtectedRoute; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 710d570..e832b90 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -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" > - @@ -99,13 +119,13 @@ const AppSidebar: React.FC = () => {
- JD + {user?.first_name ? user.first_name.charAt(0) : "U"}

- John Doe + {user?.first_name || "John Doe"} {user?.last_name || ""}

-

john@example.com

+

{user?.email}

diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index c93ecfc..e1526bd 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -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 = () => {
{/* Mobile Logo */}
-
- +
+ Logo
- AffiliatePro + Planpost Affiliate
diff --git a/src/pages/Overview.tsx b/src/pages/Overview.tsx index c69e07f..51adbfa 100644 --- a/src/pages/Overview.tsx +++ b/src/pages/Overview.tsx @@ -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 ( +
+
Loading...
+
+ ); + } + return ( -
+
{/* Header */}

@@ -73,27 +115,26 @@ const Overview = () => {
@@ -148,35 +189,40 @@ const Overview = () => {

- Conversion Rate + Average Commission

-

- 12.5% +

+ ${averageCommission.toFixed(2)}

-
-
-
+

Per referral

- Average Commission + Available Balance

-

$37.65

+

+ ${(totalEarnings - totalWithdraw).toFixed(2)} +

- ↑ $2.15 from last month + Ready to withdraw

- Next Payout + Latest Transaction

-

$945

-

In 12 days

+

+ ${affiliateData?.history[0]?.amount.toFixed(2) || "0.00"} +

+

+ {affiliateData?.history[0] + ? new Date( + affiliateData.history[0].created_at + ).toLocaleDateString() + : "No transactions yet"} +

diff --git a/src/stores/affiliateStore.ts b/src/stores/affiliateStore.ts new file mode 100644 index 0000000..39ad698 --- /dev/null +++ b/src/stores/affiliateStore.ts @@ -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; + clearData: () => void; + clearError: () => void; +} + +const API_BASE_URL = import.meta.env.VITE_SERVER_URL; + +export const useAffiliateStore = create((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 }); + }, +})); diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index cf78f1c..644de33 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -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((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((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((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((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((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((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: () => { diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts new file mode 100644 index 0000000..0a1ff56 --- /dev/null +++ b/src/stores/userStore.ts @@ -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; + clearUser: () => void; + clearError: () => void; +} + +const API_BASE_URL = import.meta.env.VITE_SERVER_URL; + +export const useUserStore = create((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 }); + }, +}));