diff --git a/package-lock.json b/package-lock.json
index 05bb5f6..563218f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,6 +23,7 @@
"marked": "^15.0.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-hot-toast": "^2.6.0",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"react-social-icons": "^6.16.0",
@@ -31,6 +32,7 @@
"web-vitals": "^2.1.4"
},
"devDependencies": {
+ "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"tailwindcss": "^3.4.17"
}
},
@@ -660,9 +662,18 @@
}
},
"node_modules/@babel/plugin-proposal-private-property-in-object": {
- "version": "7.21.0-placeholder-for-preset-env.2",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
- "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
+ "version": "7.21.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz",
+ "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==",
+ "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.18.6",
+ "@babel/helper-create-class-features-plugin": "^7.21.0",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5"
+ },
"engines": {
"node": ">=6.9.0"
},
@@ -1905,6 +1916,18 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.0-placeholder-for-preset-env.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
+ "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
"node_modules/@babel/preset-env/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -6238,9 +6261,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001596",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001596.tgz",
- "integrity": "sha512-zpkZ+kEr6We7w63ORkoJ2pOfBwBkY/bJrG/UZ90qNb45Isblu8wzDgevEOrRL1r9dWayHjYiiyCMEXPn4DweGQ==",
+ "version": "1.0.30001770",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
+ "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
"funding": [
{
"type": "opencollective",
@@ -6254,7 +6277,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
- ]
+ ],
+ "license": "CC-BY-4.0"
},
"node_modules/case-sensitive-paths-webpack-plugin": {
"version": "2.4.0",
@@ -9447,6 +9471,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/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -15593,6 +15626,23 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
},
+ "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": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -19575,10 +19625,16 @@
}
},
"@babel/plugin-proposal-private-property-in-object": {
- "version": "7.21.0-placeholder-for-preset-env.2",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
- "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
- "requires": {}
+ "version": "7.21.11",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz",
+ "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-annotate-as-pure": "^7.18.6",
+ "@babel/helper-create-class-features-plugin": "^7.21.0",
+ "@babel/helper-plugin-utils": "^7.20.2",
+ "@babel/plugin-syntax-private-property-in-object": "^7.14.5"
+ }
},
"@babel/plugin-syntax-async-generators": {
"version": "7.8.4",
@@ -20373,6 +20429,12 @@
"semver": "^6.3.1"
},
"dependencies": {
+ "@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.0-placeholder-for-preset-env.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
+ "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
+ "requires": {}
+ },
"semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -23525,9 +23587,9 @@
}
},
"caniuse-lite": {
- "version": "1.0.30001596",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001596.tgz",
- "integrity": "sha512-zpkZ+kEr6We7w63ORkoJ2pOfBwBkY/bJrG/UZ90qNb45Isblu8wzDgevEOrRL1r9dWayHjYiiyCMEXPn4DweGQ=="
+ "version": "1.0.30001770",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
+ "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw=="
},
"case-sensitive-paths-webpack-plugin": {
"version": "2.4.0",
@@ -25806,6 +25868,12 @@
"slash": "^3.0.0"
}
},
+ "goober": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
+ "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
+ "requires": {}
+ },
"gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -30017,6 +30085,15 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
},
+ "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==",
+ "requires": {
+ "csstype": "^3.1.3",
+ "goober": "^2.1.16"
+ }
+ },
"react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
diff --git a/package.json b/package.json
index 35cfced..4d76044 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"marked": "^15.0.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-hot-toast": "^2.6.0",
"react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"react-social-icons": "^6.16.0",
@@ -29,7 +30,8 @@
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
- "eject": "react-scripts eject"
+ "eject": "react-scripts eject",
+ "lint": "eslint src --ext .js,.jsx"
},
"eslintConfig": {
"extends": [
@@ -50,6 +52,7 @@
]
},
"devDependencies": {
+ "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"tailwindcss": "^3.4.17"
}
}
diff --git a/src/App.js b/src/App.js
index 9af0573..7f9999f 100644
--- a/src/App.js
+++ b/src/App.js
@@ -15,11 +15,13 @@ import {Dashboard} from "./screens/Dashboard";
import {DashboardView} from "./screens/DashboardView";
import {GraphService} from "./screens/GraphService";
import Tunnels from "./screens/Tunnels";
+import { Toaster } from "react-hot-toast";
function App() {
return (
+
} />
} />
diff --git a/src/components/AvatarMenu.js b/src/components/AvatarMenu.js
index 8819c65..173828c 100644
--- a/src/components/AvatarMenu.js
+++ b/src/components/AvatarMenu.js
@@ -1,3 +1,5 @@
+import { AuthHandler } from "../utils/authHandler";
+
function AvatarMenu({ account, initials, togglePopup, activePopup }) {
return (
@@ -28,8 +30,7 @@ function AvatarMenu({ account, initials, togglePopup, activePopup }) {
);
}
-export default Footer;
+export default Footer;
\ No newline at end of file
diff --git a/src/components/FormControls.js b/src/components/FormControls.js
index efd930b..c71d427 100644
--- a/src/components/FormControls.js
+++ b/src/components/FormControls.js
@@ -91,4 +91,10 @@ export const DropdownSelect = ({
)
}
-export default { TextInput, NumberInput, DropdownSelect }
+const FormControls = {
+ TextInput,
+ NumberInput,
+ DropdownSelect,
+}
+
+export default FormControls
\ No newline at end of file
diff --git a/src/components/HomeComponent.js b/src/components/HomeComponent.js
index 504433b..76e6ad6 100644
--- a/src/components/HomeComponent.js
+++ b/src/components/HomeComponent.js
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import { useState } from "react";
export default function LockBoxLanding() {
const [faqOpen, setFaqOpen] = useState(null);
@@ -40,15 +40,6 @@ export default function LockBoxLanding() {
},
];
- const [copied, setCopied] = useState(false);
- const bashScript = `/bin/bash -c "$(curl -fsSL https://github.com/avinash84319/providerServer/install.sh)"`;
-
- const handleCopy = () => {
- navigator.clipboard.writeText(bashScript);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000); // reset after 2 seconds
- };
-
return (
{/* Header Section */}
diff --git a/src/components/Navbar.js b/src/components/Navbar.js
index e92502b..cf45c78 100644
--- a/src/components/Navbar.js
+++ b/src/components/Navbar.js
@@ -7,7 +7,6 @@ import useNavbarScroll from "../hooks/useNavbarScroll";
import { getInitials } from "../utils/userUtils";
import useClickOutside from "../hooks/useClickOutside";
-
const Navbar = () => {
const [activePopup, setActivePopup] = useState(null);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
diff --git a/src/docs/components/docsMainPage.js b/src/docs/components/docsMainPage.js
index 7e5d6fc..a46e7c5 100644
--- a/src/docs/components/docsMainPage.js
+++ b/src/docs/components/docsMainPage.js
@@ -1,10 +1,8 @@
import React, { useState } from 'react';
-import Navbar from '../../components/Navbar';
import Sidebar from './sidebar';
import ShowDocumentation from './showDocumentation';
import Mainheadings from './mainheadings';
import "../css/docsMainPage.css";
-import { file } from 'jszip';
const DocsMainPage = () => {
diff --git a/src/screens/Buckets.js b/src/screens/Buckets.js
index f513831..090c6f4 100644
--- a/src/screens/Buckets.js
+++ b/src/screens/Buckets.js
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
-import JSZip from 'jszip'; // Import JSZip for zipping folders
+import JSZip from 'jszip';
import { apiCall } from '../Api';
const Buckets = () => {
@@ -57,7 +57,7 @@ const Buckets = () => {
const zipFolder = async (files) => {
const zip = new JSZip();
- const folderName = path.split('/').pop(); // Folder name
+ // const folderName = path.split('/').pop(); // Folder name
const fileArray = Array.from(files); // ✅ Convert FileList to array
fileArray.forEach(file => {
// Add each file to the zip
diff --git a/src/screens/DashboardView.js b/src/screens/DashboardView.js
index e68dd72..9c5d0e8 100644
--- a/src/screens/DashboardView.js
+++ b/src/screens/DashboardView.js
@@ -1,7 +1,7 @@
-import React, { useEffect, useState, useRef } from 'react'
+import React, { useEffect, useState } from 'react'
import { GraphView } from '../components/GraphView'
import DashboardCreateModal from '../components/DashboardCreateModal'
-import { useNavigate, useSearchParams } from 'react-router-dom'
+import { useSearchParams } from 'react-router-dom'
import { Trash2 } from 'lucide-react'
import Toast from '../components/ToastService'
import ActionConfirmModal from '../components/actionConfirmModal'
@@ -11,7 +11,6 @@ import { epochToReadable } from '../helper'
import { listGraphsForDashboard, deleteGraph, getGraphPoints } from '../apiServices'
export const DashboardView = () => {
- const navigate = useNavigate()
const [searchParams] = useSearchParams()
const dashboardId = searchParams.get('id')
const [graphs, setGraphs] = useState([])
diff --git a/src/screens/Login.js b/src/screens/Login.js
index 354a034..a81a40d 100644
--- a/src/screens/Login.js
+++ b/src/screens/Login.js
@@ -1,7 +1,9 @@
-import { apiCall } from '../Api';
-import { Link } from 'react-router-dom';
-import { useState } from 'react';
-
+import { apiCall } from "../Api";
+import { Link } from "react-router-dom";
+import { useState } from "react";
+import { validators } from "../utils/validators";
+import { logger } from "../utils/logger";
+import {AuthHandler} from "../utils/authHandler";
function Login() {
const [error, setError] = useState(null);
@@ -9,66 +11,107 @@ function Login() {
const handlesubmit = async (e) => {
e.preventDefault();
- const usernameOrEmail = e.target[0].value;
- const password = e.target[1].value;
+
+ const usernameOrEmail = e.target.username.value;
+ const password = e.target.password.value;
+
+ // validation
+ let validationError = null;
+
+ if (usernameOrEmail.includes("@")) {
+ validationError = validators.email(usernameOrEmail);
+ } else {
+ validationError = validators.username(usernameOrEmail);
+ }
+
+ if (!validationError) {
+ validationError = validators.password(password);
+ }
+
+ if (validationError) {
+ setError(validationError);
+ logger.error(validationError);
+ return;
+ }
+
try {
- setIsLoading(true)
- const res = await apiCall("post", `/login`, { username_or_email: usernameOrEmail, password: password });
- setIsLoading(false);
- console.log("Login successful:", res.token);
- console.log(res)
- sessionStorage.setItem('token', res.token);
- window.location.href = "/home";
- } catch (error) {
+ setIsLoading(true);
+ setError(null);
+
+ const res = await apiCall("post", `/login`, {
+ username_or_email: usernameOrEmail,
+ password: password,
+ });
+
+ AuthHandler.login(res.token);
+
+ logger.success("Login successful");
+ } catch (err) {
+ const message =
+ err ||
+ "Login failed";
+
+ setError(message);
+ logger.error(message);
+
+
+ } finally {
setIsLoading(false);
- // alert(error);
- setError(error);
}
};
-
-
return (
<>
-
-
{/* Left - Login Form */}
- {isLoading && (
-
- )}
-
Login
-
-
{error}
+
+ {error}
+
Terms & Conditions
- Don’t have an account?{' '}
-
+ Don’t have an account?{" "}
+
Register
@@ -86,8 +129,7 @@ function Login() {
>
- )
+ );
}
-
-export default Login;
\ No newline at end of file
+export default Login;
diff --git a/src/screens/Register.js b/src/screens/Register.js
index cfbe480..f818b1e 100644
--- a/src/screens/Register.js
+++ b/src/screens/Register.js
@@ -1,103 +1,147 @@
import { Link } from "react-router-dom";
import { apiCall } from "../Api";
import { useState } from "react";
+import { logger } from "../utils/logger";
+import { validators } from "../utils/validators";
+import {AuthHandler} from "../utils/authHandler";
+
function Register() {
- const [error, setError] = useState(null);
- const [isLoading, setIsLoading] = useState(false)
-
- const handlesubmit = async(e) => {
- e.preventDefault();
- if (e.target[3].value === e.target[2].value) {
- const email = e.target[0].value;
- const username = e.target[1].value;
- const password = e.target[2].value;
- try {
- setIsLoading(true)
- await apiCall("post", "/register", { email: email, username: username, password: password }).then((res) => {
- console.log(res);
- window.location.href = '/login';
- })
- } catch (error) {
- // alert(error);
- setError(error);
- }
- finally{
- setIsLoading(false)
- }
- }
- else {
- alert('Passwords do not match');
- }
- }
-
- return (
-
-
-
- {/* Left - Registration Form */}
-
+const [error, setError] = useState(null);
+const [isLoading, setIsLoading] = useState(false);
+
+const handleError = (message) => {
+ setError(message);
+ logger.error(message);
+};
+
+const handleSubmit = async (event) => {
+ event.preventDefault();
+ setError(null);
+
+ const { email, username, password, confirmPassword } = event.target;
+
+ const emailValue = email.value.trim();
+ const usernameValue = username.value.trim();
+ const passwordValue = password.value;
+ const confirmPasswordValue = confirmPassword.value;
+
+ // VALIDATION
+ const usernameError = validators.username(usernameValue);
+ if (usernameError) return handleError(usernameError);
+
+ const emailError = validators.email(emailValue);
+ if (emailError) return handleError(emailError);
+
+ const passwordError = validators.password(passwordValue);
+ if (passwordError) return handleError(passwordError);
+
+ if (passwordValue !== confirmPasswordValue) {
+ return handleError("Passwords do not match");
+ }
+
+ try {
+ setIsLoading(true);
+
+ const res = await apiCall("post", "/register", {
+ email: emailValue,
+ username: usernameValue,
+ password: passwordValue,
+ });
+
+ logger.success("Registered successfully");
+
+ AuthHandler.login(res.token);
+ } catch (err) {
+ const message =
+ err ||
+ "Registration failed";
+
+ handleError(message);
+ } finally {
+ setIsLoading(false);
+ }
+};
+ return (
+
+
+ {/* Left - Registration Form */}
+
{isLoading && (
-
- )}
-
Register
-
-
-
-
{error}
-
-
-
- Already registered?{' '}
-
- Login here
-
-
+
-
-
- {/* Right - Image (hidden on mobile) */}
-
-

+ Register
+
+
+
+
+
+ {error}
+
+
+
+
+ Already registered?{" "}
+
+ Login here
+
+
+
+ {/* Right - Image (hidden on mobile) */}
+
+

+
-
- );
+
+ );
}
-export default Register;
\ No newline at end of file
+export default Register;
diff --git a/src/utils/authHandler.js b/src/utils/authHandler.js
new file mode 100644
index 0000000..3c1bb08
--- /dev/null
+++ b/src/utils/authHandler.js
@@ -0,0 +1,28 @@
+import Cookies from "js-cookie";
+
+export const AuthHandler = {
+ login: (token) => {
+ sessionStorage.setItem("token", token);
+
+ Cookies.set("token", token, {
+ expires: 1,
+ path: "/",
+ secure: true,
+ sameSite: "Strict",
+ });
+
+ window.location.replace("/home");
+ },
+
+ logout: () => {
+ sessionStorage.removeItem("token");
+
+ Cookies.remove("token");
+
+ window.location.replace("/login");
+ },
+
+ getToken: () => {
+ return Cookies.get("token");
+ },
+};
\ No newline at end of file
diff --git a/src/utils/logger.js b/src/utils/logger.js
new file mode 100644
index 0000000..0bd641b
--- /dev/null
+++ b/src/utils/logger.js
@@ -0,0 +1,7 @@
+import toast from "react-hot-toast";
+
+export const logger = {
+ success: (message) => toast.success(message),
+ error: (message) => toast.error(message),
+ info: (message) => toast(message),
+};
\ No newline at end of file
diff --git a/src/utils/validators.js b/src/utils/validators.js
new file mode 100644
index 0000000..520b27f
--- /dev/null
+++ b/src/utils/validators.js
@@ -0,0 +1,28 @@
+export const validators = {
+ username: (value) => {
+ if (!value || value.trim().length < 3) {
+ return "Username must be at least 3 characters";
+ }
+ return null;
+ },
+
+ email: (value) => {
+ const emailRegex =
+ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
+
+ if (!value) return "Email is required";
+
+ if (!emailRegex.test(value)) {
+ return "Invalid email format";
+ }
+
+ return null;
+ },
+
+ password: (value) => {
+ if (!value || value.length < 6) {
+ return "Password must be at least 6 characters";
+ }
+ return null;
+ },
+};
\ No newline at end of file