added landing page

This commit is contained in:
Adspillar 2025-10-06 14:59:03 +06:00
commit 9c27a8619b
7 changed files with 294 additions and 21 deletions

1
.env Normal file
View file

@ -0,0 +1 @@
VITE_SERVER_URL=https://backend.planpostai.com/api/v2

60
package-lock.json generated
View file

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

View file

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

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

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

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