diff --git a/.env b/.env
new file mode 100644
index 0000000..8cf65dd
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+VITE_SERVER_URL=https://backend.planpostai.com/api/v2
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 1170812..904f900 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index 1dd6a9a..30fed95 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/App.tsx b/src/App.tsx
index f3db33d..7bec0b6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -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 = () => {
} />
+
);
};
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx
index 03c243a..c93ecfc 100644
--- a/src/pages/Login.tsx
+++ b/src/pages/Login.tsx
@@ -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 (
diff --git a/src/pages/Signup.tsx b/src/pages/Signup.tsx
index e5c9cf2..c17758b 100644
--- a/src/pages/Signup.tsx
+++ b/src/pages/Signup.tsx
@@ -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 (
{/* Left Side - Branding */}
@@ -139,9 +180,9 @@ const Signup: React.FC = () => {
- {error && (
+ {displayError && (
-
{error}
+
{displayError}
)}
@@ -180,6 +221,24 @@ const Signup: React.FC = () => {
+
+
+
+
+
+ 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"
+ />
+
+
+