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",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-hot-toast": "^2.6.0",
"react-router": "^7.9.3",
"react-router-dom": "^7.9.3",
"recharts": "^3.2.1",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13"
"tailwindcss": "^4.1.13",
"zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
@ -2924,8 +2926,8 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/d3-array": {
"version": "3.2.4",
@ -3557,6 +3559,15 @@
"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": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -4346,6 +4357,23 @@
"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": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz",
@ -5181,6 +5209,35 @@
"funding": {
"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",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-hot-toast": "^2.6.0",
"react-router": "^7.9.3",
"react-router-dom": "^7.9.3",
"recharts": "^3.2.1",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13"
"tailwindcss": "^4.1.13",
"zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/js": "^9.36.0",

View file

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

View file

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

View file

@ -7,6 +7,9 @@ import {
ArrowRight,
User,
} 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 [form, setForm] = useState({
@ -15,28 +18,48 @@ const Signup: React.FC = () => {
password: "",
confirm: "",
});
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [localError, setLocalError] = useState("");
const navigate = useNavigate();
const { register, isLoading, error: storeError } = useAuthStore();
const handleSubmit = async (e: React.FormEvent) => {
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) {
return setError("Passwords do not match");
return setLocalError("Passwords do not match");
}
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);
setError("");
setLocalError("");
setTimeout(() => {
setIsLoading(false);
alert("Account created successfully! Redirecting to login...");
}, 1200);
// Split name into first and last name
const nameParts = form.name.trim().split(" ");
const firstName = nameParts[0];
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 =
@ -48,6 +71,8 @@ const Signup: React.FC = () => {
: "strong"
: null;
const displayError = localError || storeError;
return (
<div className="min-h-screen flex">
{/* Left Side - Branding */}
@ -139,9 +164,9 @@ const Signup: React.FC = () => {
</p>
</div>
{error && (
{displayError && (
<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>
)}
@ -262,7 +287,7 @@ const Signup: React.FC = () => {
<input
type="checkbox"
id="terms"
className="w-4 h-4 border-gray-300 rounded focus:ring-indigo-500 mt-1 bg-white"
className="w-4 h-4 border-gray-300 rounded focus:ring-indigo-500 mt-1 bg-white"
/>
<label htmlFor="terms" className="ml-2 text-sm text-gray-700">
I agree to the{" "}

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