This commit is contained in:
S M Fahim Hossen 2025-10-05 17:03:50 +06:00
parent 6dee2e5b3a
commit 3391a14643
6 changed files with 260 additions and 22 deletions

63
package-lock.json generated
View file

@ -18,11 +18,13 @@
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-hot-toast": "^2.6.0",
"react-router": "^7.9.3", "react-router": "^7.9.3",
"react-router-dom": "^7.9.3", "react-router-dom": "^7.9.3",
"recharts": "^3.2.1", "recharts": "^3.2.1",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13" "tailwindcss": "^4.1.13",
"zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",
@ -2924,8 +2926,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==",
"devOptional": true, "license": "MIT",
"license": "MIT" "peer": true
}, },
"node_modules/d3-array": { "node_modules/d3-array": {
"version": "3.2.4", "version": "3.2.4",
@ -3557,6 +3559,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/goober": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -4346,6 +4357,23 @@
"react": "^19.1.1" "react": "^19.1.1"
} }
}, },
"node_modules/react-hot-toast": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "19.1.1", "version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
@ -5181,6 +5209,35 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zustand": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
} }
} }
} }

View file

@ -20,11 +20,13 @@
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-hot-toast": "^2.6.0",
"react-router": "^7.9.3", "react-router": "^7.9.3",
"react-router-dom": "^7.9.3", "react-router-dom": "^7.9.3",
"recharts": "^3.2.1", "recharts": "^3.2.1",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13" "tailwindcss": "^4.1.13",
"zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",

View file

@ -6,6 +6,7 @@ import DashboardLayout from "./components/layouts/DashboardLayouts";
import Overview from "./pages/Overview"; import Overview from "./pages/Overview";
import Referrals from "./pages/Referrals"; import Referrals from "./pages/Referrals";
import Earnings from "./pages/Earnings"; import Earnings from "./pages/Earnings";
import { Toaster } from "react-hot-toast";
const App: React.FC = () => { const App: React.FC = () => {
return ( return (
@ -21,6 +22,7 @@ const App: React.FC = () => {
<Route path="/dashboard/earnings" element={<Earnings />} /> <Route path="/dashboard/earnings" element={<Earnings />} />
</Route> </Route>
</Routes> </Routes>
<Toaster position="top-right" reverseOrder={false} />
</BrowserRouter> </BrowserRouter>
); );
}; };

View file

@ -1,12 +1,14 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { TrendingUp, Mail, Lock, ArrowRight } from "lucide-react"; import { TrendingUp, Mail, Lock, ArrowRight } from "lucide-react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { useAuthStore } from "@/stores/authStore";
const Login: React.FC = () => { const Login: React.FC = () => {
const [form, setForm] = useState({ email: "", password: "" }); const [form, setForm] = useState({ email: "", password: "" });
const [error, setError] = useState(""); const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const { login } = useAuthStore();
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@ -15,12 +17,12 @@ const Login: React.FC = () => {
} }
setIsLoading(true); setIsLoading(true);
setError(""); setError("");
const result = await login(form.email, form.password);
// Simulate API call if (result.success) {
setTimeout(() => {
setIsLoading(false); setIsLoading(false);
navigate("/dashboard/overview"); navigate("/dashboard/overview");
}, 1000); }
}; };
return ( return (

View file

@ -7,6 +7,9 @@ import {
ArrowRight, ArrowRight,
User, User,
} from "lucide-react"; } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useAuthStore } from "@/stores/authStore";
import toast from "react-hot-toast";
const Signup: React.FC = () => { const Signup: React.FC = () => {
const [form, setForm] = useState({ const [form, setForm] = useState({
@ -15,28 +18,48 @@ const Signup: React.FC = () => {
password: "", password: "",
confirm: "", confirm: "",
}); });
const [error, setError] = useState(""); const [localError, setLocalError] = useState("");
const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate();
const { register, isLoading, error: storeError } = useAuthStore();
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!form.email || !form.password || !form.confirm) {
return setError("All fields are required"); if (!form.name || !form.email || !form.password || !form.confirm) {
return setLocalError("All fields are required");
} }
if (form.password !== form.confirm) { if (form.password !== form.confirm) {
return setError("Passwords do not match"); return setLocalError("Passwords do not match");
} }
if (form.password.length < 8) { if (form.password.length < 8) {
return setError("Password must be at least 8 characters"); return setLocalError("Password must be at least 8 characters");
} }
setIsLoading(true); setLocalError("");
setError("");
setTimeout(() => { // Split name into first and last name
setIsLoading(false); const nameParts = form.name.trim().split(" ");
alert("Account created successfully! Redirecting to login..."); const firstName = nameParts[0];
}, 1200); const lastName = nameParts.slice(1).join(" ");
// Call Zustand register action
const result = await register({
email: form.email,
first_name: firstName,
last_name: lastName || undefined,
password: form.password,
role: "affiliate",
});
if (result.success) {
// Redirect to login after successful registration
toast("Account created successfully! Please log in.");
navigate("/login");
} else {
// Display error from API
setLocalError(result.error || "Registration failed");
}
}; };
const passwordStrength = const passwordStrength =
@ -48,6 +71,8 @@ const Signup: React.FC = () => {
: "strong" : "strong"
: null; : null;
const displayError = localError || storeError;
return ( return (
<div className="min-h-screen flex"> <div className="min-h-screen flex">
{/* Left Side - Branding */} {/* Left Side - Branding */}
@ -139,9 +164,9 @@ const Signup: React.FC = () => {
</p> </p>
</div> </div>
{error && ( {displayError && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg"> <div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600 text-sm">{error}</p> <p className="text-red-600 text-sm">{displayError}</p>
</div> </div>
)} )}

150
src/stores/authStore.ts Normal file
View file

@ -0,0 +1,150 @@
// store/authStore.ts
import { create } from "zustand";
interface User {
id?: string;
email: string;
first_name: string;
last_name?: string;
role: string;
}
interface AuthState {
token: string | null;
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
// Actions
login: (
email: string,
password: string
) => Promise<{ success: boolean; error?: string }>;
register: (
data: RegisterData
) => Promise<{ success: boolean; error?: string }>;
logout: () => void;
clearError: () => void;
}
interface RegisterData {
email: string;
first_name: string;
last_name?: string;
password: string;
role?: string;
phone?: string;
city?: string;
country?: string;
address?: string;
profile_photo?: File;
}
const API_BASE_URL = "http://localhost:9000/api/v2";
export const useAuthStore = create<AuthState>((set) => ({
token: null,
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
login: async (email: string, password: string) => {
set({ isLoading: true, error: null });
try {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: "POST",
headers: {
accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Login failed");
}
const data = await response.json();
// Store token (adjust based on your API response structure)
const token = data.token || data.access_token || data.data?.token;
const user = data.user ||
data.data?.user || { email, first_name: "", role: "affiliate" };
set({
token,
user,
isAuthenticated: true,
isLoading: false,
error: null,
});
return { success: true };
} catch (error) {
console.error("Registration error:", error);
return { success: false, error: "Registration error" };
}
},
register: async (userData: RegisterData) => {
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("role", userData.role || "affiliate");
// Optional fields
if (userData.last_name) formData.append("last_name", userData.last_name);
if (userData.phone) formData.append("phone", userData.phone);
if (userData.city) formData.append("city", userData.city);
if (userData.country) formData.append("country", userData.country);
if (userData.address) formData.append("address", userData.address);
if (userData.profile_photo)
formData.append("profile_photo", userData.profile_photo);
const response = await fetch(`${API_BASE_URL}/users`, {
method: "POST",
headers: {
accept: "application/json",
},
body: formData,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || "Registration failed");
}
const data = await response.json();
console.log("Registration successful:", data);
set({ isLoading: false, error: null });
return { success: true };
} catch (error) {
console.error("Registration error:", error);
return { success: false, error: "Registration error" };
}
},
logout: () => {
set({
token: null,
user: null,
isAuthenticated: false,
error: null,
});
},
clearError: () => {
set({ error: null });
},
}));