added landing page
This commit is contained in:
commit
9c27a8619b
7 changed files with 294 additions and 21 deletions
1
.env
Normal file
1
.env
Normal file
|
|
@ -0,0 +1 @@
|
|||
VITE_SERVER_URL=https://backend.planpostai.com/api/v2
|
||||
60
package-lock.json
generated
60
package-lock.json
generated
|
|
@ -19,11 +19,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",
|
||||
|
|
@ -2918,7 +2920,6 @@
|
|||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
|
|
@ -3577,6 +3578,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",
|
||||
|
|
@ -4379,6 +4389,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",
|
||||
|
|
@ -5208,6 +5235,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
import Landing from "./pages/Landing";
|
||||
|
||||
const App: React.FC = () => {
|
||||
|
|
@ -23,6 +24,7 @@ const App: React.FC = () => {
|
|||
<Route path="/dashboard/earnings" element={<Earnings />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
<Toaster position="top-right" reverseOrder={false} />
|
||||
</BrowserRouter>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -6,37 +6,76 @@ import {
|
|||
CheckCircle,
|
||||
ArrowRight,
|
||||
User,
|
||||
Phone,
|
||||
} 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({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
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.phone ||
|
||||
!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("");
|
||||
// Basic phone validation
|
||||
const phoneRegex =
|
||||
/^[+]?[(]?[0-9]{1,4}[)]?[-\s.]?[(]?[0-9]{1,4}[)]?[-\s.]?[0-9]{1,9}$/;
|
||||
if (!phoneRegex.test(form.phone)) {
|
||||
return setLocalError("Please enter a valid phone number");
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
alert("Account created successfully! Redirecting to login...");
|
||||
}, 1200);
|
||||
setLocalError("");
|
||||
|
||||
// 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,
|
||||
phone: form.phone,
|
||||
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 +87,8 @@ const Signup: React.FC = () => {
|
|||
: "strong"
|
||||
: null;
|
||||
|
||||
const displayError = localError || storeError;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex">
|
||||
{/* Left Side - Branding */}
|
||||
|
|
@ -139,9 +180,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>
|
||||
)}
|
||||
|
||||
|
|
@ -180,6 +221,24 @@ const Signup: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Phone Number
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="+1 (555) 000-0000"
|
||||
value={form.phone}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, phone: e.target.value })
|
||||
}
|
||||
className="w-full pl-11 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Password
|
||||
|
|
@ -262,7 +321,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{" "}
|
||||
|
|
|
|||
151
src/stores/authStore.ts
Normal file
151
src/stores/authStore.ts
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
// 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 = import.meta.env.VITE_SERVER_URL;
|
||||
|
||||
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();
|
||||
|
||||
const token = data.token || data.access_token || data.data?.token;
|
||||
const user = data.user ||
|
||||
data.data?.user || { email, first_name: "", role: "affiliate" };
|
||||
|
||||
localStorage.setItem("aff-token", token);
|
||||
localStorage.setItem("aff-user", JSON.stringify(user));
|
||||
|
||||
set({
|
||||
token,
|
||||
user,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Login error:", error);
|
||||
|
||||
// Clear localStorage on failure
|
||||
localStorage.removeItem("aff-token");
|
||||
localStorage.removeItem("aff-user");
|
||||
|
||||
set({ isLoading: false, error: "Login failed" });
|
||||
|
||||
return { success: false, error: "Login failed" };
|
||||
}
|
||||
},
|
||||
|
||||
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("phone", userData.phone || "");
|
||||
formData.append("role", userData.role || "affiliate");
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/auth/register`, {
|
||||
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 });
|
||||
},
|
||||
}));
|
||||
Loading…
Add table
Reference in a new issue